Laden...

Nachschlagetabelle mit String-Schlüssel + Wechsel des Arbeitsbereichs = Oberflächlicher Datenverlust

Erstellt von bb1898 vor 7 Jahren Letzter Beitrag vor 7 Jahren 2.201 Views
B
bb1898 Themenstarter:in
112 Beiträge seit 2008
vor 7 Jahren
Nachschlagetabelle mit String-Schlüssel + Wechsel des Arbeitsbereichs = Oberflächlicher Datenverlust

Die Situation:

Das Hauptfenster der Anwendung enthält einen Bereich für wechselnde UserControl-Instanzen, die für verschiedene Tätigkeiten da sind. Außerdem Schalter zum Wechseln der Tätigkeit.

Ein UserControl ist dazu da, Sätze aus einer Liste anzuzeigen, einen davon auszuwählen und zu bearbeiten. Die Tabelle hat drei Fremdschlüssel, die sie mit drei kurzen und stabilen Nachschlagetabellen verknüpfen; zum richtigen Setzen der Fremdschlüssel gibt es jeweils eine Combobox, die die Sätze der Nachschlagetabelle zeigt. Eine der Nachschlagetabellen hat einen kurzen String als Primärschlüssel und das scheint der Knackpunkt zu sein.

Das arbeitet alles so, wie es soll, so lange ich in diesem einen Arbeitsbereich bleibe. Nennen wir das mal das Bearbeitungsfenster. Das Umschalten auf einen anderen Bereich klappt auch, nur:

Wenn ich vom Bearbeitungsfenster weg- und dann wieder zu ihm hinschalte, dann ist in der Combobox, die zu dem String-Schlüssel gehört, nichts mehr ausgewählt. Im Debugger sehe ich, dass der entsprechende Fremdschlüssel in diesem Satz jetzt null ist. Der ViewModel-Typ, in den die EF-Entities eingepackt sind, erlaubt das Wiederherstellen der Originaldaten, und das stellt dann auch den Fremdschlüssel richtig wieder her. Komplett verschwunden ist er also nicht.

Ich würde gern wissen, ob jemand schon mal auf so eine Sorte Problem gestoßen ist und mehr über die möglichen Ursachen weiß als ich. Passende Suchbegriffe wären auch eine große Hilfe; ich habe auf StackOverflow hauptsächlich nach Fragen zur Combobox bzw. zu "SelectedValue" gesucht, aber das hat nichts ergeben.

Abhilfemöglichkeiten, die kein Verständnis des Problems erfordern, sehe ich mehrere, Verständnis wäre mir aber wichtiger.

Ein Minimalbeispiel, das das Problem demonstriert, benutzt Daten aus einer XML-Datei, nicht aus einer Datenbank, und nur zwei, nicht drei Nachschlagetabellen. In seinen Modellklassen gibt es keine Navigationseigenschaften, nur die Properties für die Fremdschlüssel (so weit man ohne Datenbank von solchen überhaupt reden sollte). Es hat nur Modellklassen, die INPC implementieren, keine drum herum gepackten ViewModel-Klassen; und es gibt keine Bearbeitung, die Sätze werden nur angesehen.

Die wichtigsten Teile des Minimalbeispiels:

Haupt-Datenklasse (Basisklasse VMBase implementiert INPC, sonst tut sie nichts)


public class MainItem : VMBase
{
	private int _id;
	private string _name;
	private double _price;
	private int _iLookupId;
	private string _sLookupId;

	public int Id {
		get { return _id; }
		set { SetProperty(ref _id, value); }
	}
	public string Name {
		get { return _name; }
		set { SetProperty(ref _name, value); }
	}
	public double Price
	{
		get { return _price; }
		set { SetProperty(ref _price, value); }
	}
	public int ILookupId
	{
		get { return _iLookupId; }
		set { SetProperty(ref _iLookupId, value); }
	}
	public string SLookupId
	{
		get { return _sLookupId; }
		set { SetProperty(ref _sLookupId, value); }
	}
}

Nachschlagetabelle mit Integer-Schlüssel


public class ILookup
{
	public int Id { get; set; }
	public string Name { get; set; }
}

Nachschlagetabelle mit String-Schlüssel


public class SLookup
{
	public string Id { get; set; }
	public string Name { get; set; }
}

Einlesen und Bereithalten der Daten:


public class DataAccess
{
	const string xmlname = @"D:\home\sibylle\dokumente\dbspiel\sibdata_1.xml";

	public ICollection<MainItem> MainItems { get; private set; }
	public ICollection<ILookup> ILookups { get; private set; }
	public ICollection<SLookup> SLookups { get; private set; }
	public int MaxId { get; private set; }

	public DataAccess()
	{
		ILookups = new List<ILookup>();
		SLookups = new List<SLookup>();
		MainItems = new List<MainItem>();
		LoadData();
	}

	private void LoadData()
	{
		... // Lesen aus einer XML-Datei, verteilen auf die drei Listen
	}

ViewModel für den Bearbeitungsbereich (IPageVM ist eine Schnittstelle, die nur die Eigenschaft PageName bereitstellt; wird für das Umschalten zwischen Arbeitsbereichen im Hauptfenster benutzt)


class DataPageVM : VMBase, IPageVM
{
	private DataAccess _data;
	private MainItem _currentMainItem;

	public string PageName { get { return "Datenseite"; } }
	public ObservableCollection<MainItem> AllMainItems { get; private set; }
	public List<ILookup> AllILookups { get; private set; }
	public List<SLookup> AllSLookups { get; private set; }

	public MainItem CurrentMainItem
	{
		get { return _currentMainItem; }
		set { SetProperty(ref _currentMainItem, value); }
	}

	public DataPageVM()
	{
		_data = new DataAccess();
		AllMainItems = new ObservableCollection<MainItem>(_data.MainItems);
		AllILookups = _data.ILookups as List<ILookup>;
		AllSLookups = _data.SLookups as List<SLookup>;
		CurrentMainItem = AllMainItems.First();
	}
}

Und die XAML-Datei für den Bearbeitungsbereich


<UserControl x:Class="UCWechsel_XML.DataUC"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:UCWechsel_XML"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Grid Background="LightGoldenrodYellow">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="2*"/>
                <ColumnDefinition Width="3*"/>
            </Grid.ColumnDefinitions>
            <Border BorderBrush="DarkGreen" BorderThickness="1" Margin="5">
                <StackPanel>
                    <Label Content="Alle Sätze" />
                    <ListBox ItemsSource="{Binding AllMainItems}"
                     SelectedItem="{Binding CurrentMainItem}"
                     DisplayMemberPath="Name"
                     IsSynchronizedWithCurrentItem="True" />
                </StackPanel>
            </Border>
            <Border Grid.Column="1" BorderBrush="DarkGreen" BorderThickness="1" Margin="5">
                <StackPanel>
                    <Label Content="Details" />
                    <Grid DataContext="{Binding CurrentMainItem}" >
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto" />
                            <ColumnDefinition />
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                        </Grid.RowDefinitions>
                        <Label Content="SLookup" />
                        <ComboBox Grid.Column="1" 
                                  ItemsSource="{Binding DataContext.AllSLookups, 
                                      RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
                                  DisplayMemberPath="Name"
                                  SelectedValuePath="Id"
                                  SelectedValue="{Binding SLookupId}" VerticalContentAlignment="Center" />
                        <Label Grid.Row="1" Content="ILookup" />
                        <ComboBox Grid.Row="1" Grid.Column="1" 
                                  ItemsSource="{Binding DataContext.AllILookups, 
                                      RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
                                  DisplayMemberPath="Name"
                                  SelectedValuePath="Id"
                                  SelectedValue="{Binding ILookupId}" VerticalContentAlignment="Center" />
                        <Label Grid.Row="2" Content="Name" />
                        <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Name}" VerticalContentAlignment="Center" />
                        <Label Grid.Row="3" Content="Price" />
                        <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Price}" VerticalContentAlignment="Center" />
                        <TextBlock Grid.Row="4" Text="ID" Margin="0,3" Padding="5" />
                        <TextBlock Grid.Row="4" Grid.Column="1" Text="{Binding Id}" Margin="0,3" VerticalAlignment="Center"/>
                    </Grid>
                </StackPanel>
            </Border>
        </Grid>
    </Grid>
</UserControl>

5.299 Beiträge seit 2008
vor 7 Jahren

Hi!

Kannst du das Minimalbeispiel vlt. zippen und anhängen (also inklusive xml, aber ohne Binaries)?

Ich bin zu faul, es nachzubasteln, womöglich falsch, wo ich doch weiß, dasses bei dir eins gibt, was den Fehler zuverlässig reproduziert.

Der frühe Apfel fängt den Wurm.

B
bb1898 Themenstarter:in
112 Beiträge seit 2008
vor 7 Jahren

Da ist es. Falls etwas fehlt, was nötig wäre, bitte sagen! Die .sln-Datei habe ich erst mal weggelassen, weil die gleiche Projektmappe noch ein weniger minimales Beispiel enthält; bei Bedarf nehme ich das Zeugs noch mal auseinander.

Jedenfalls Danke für's Hineinschauen!

W
955 Beiträge seit 2010
vor 7 Jahren

Hallo,

ich habe dein xaml mal etwas aufgeräumt, es funktioniert wenn nicht zwei verschiedene DataContexts verwendet werden.


<UserControl x:Class="UCWechsel_XML.DataUC"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:UCWechsel_XML"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Grid Background="LightGoldenrodYellow">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="2*"/>
                <ColumnDefinition Width="3*"/>
            </Grid.ColumnDefinitions>
            <Border BorderBrush="DarkGreen" BorderThickness="1" Margin="5">
                <StackPanel>
                    <Label Content="Alle Sätze" />
                    <ListBox ItemsSource="{Binding AllMainItems}"
                     SelectedItem="{Binding CurrentMainItem}"
                     DisplayMemberPath="Name"
                     IsSynchronizedWithCurrentItem="True" />
                </StackPanel>
            </Border>
            <Border Grid.Column="1" BorderBrush="DarkGreen" BorderThickness="1" Margin="5">
                <StackPanel>
                    <Label Content="Details" />
                    <Grid  >
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto" />
                            <ColumnDefinition />
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                        </Grid.RowDefinitions>
                        <Label Content="SLookup" />
                        <ComboBox Grid.Column="1" 
                                  ItemsSource="{Binding AllSLookups}"
                                  DisplayMemberPath="Name"
                                  SelectedValuePath="Id"
                                  SelectedValue="{Binding CurrentMainItem.SLookupId}" VerticalContentAlignment="Center" />
                        <Label Grid.Row="1" Content="ILookup" />
                        <ComboBox Grid.Row="1" Grid.Column="1" 
                                  ItemsSource="{Binding AllILookups}"
                                  DisplayMemberPath="Name"
                                  SelectedValuePath="Id"
                                  SelectedValue="{Binding CurrentMainItem.ILookupId}" VerticalContentAlignment="Center" />
                        <Label Grid.Row="2" Content="Name" />
                        <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding CurrentMainItem.Name}" VerticalContentAlignment="Center" />
                        <Label Grid.Row="3" Content="Price" />
                        <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding CurrentMainItem.Price}" VerticalContentAlignment="Center" />
                        <TextBlock Grid.Row="4" Text="ID" Margin="0,3" Padding="5" />
                        <TextBlock Grid.Row="4" Grid.Column="1" Text="{Binding CurrentMainItem.Id}" Margin="0,3" VerticalAlignment="Center"/>
                    </Grid>
                </StackPanel>
            </Border>
        </Grid>
    </Grid>
</UserControl>

B
bb1898 Themenstarter:in
112 Beiträge seit 2008
vor 7 Jahren

So ist es. Auch mein ursprüngliches Programm funktioniert mit dieser Änderung. Aber warum? Das verstehe ich genau so wenig wie ich vorher den Fehler verstanden habe.

Dass das XAML so deutlich aufgeräumter aussieht, stimmt; allerdings ist die Entsprechung zum "MainItem" im ursprünglichen Programm ein Objekt mit deutlich mehr Properties, die zu bearbeiten sind - und die brauchen jetzt alle die längere Pfadangabe. Deswegen ja der Extra-Kontext.

Kann jemand in meinem ursprünglichen XAML einen Fehler sehen? Prinzipiell falsch ist ein eigener DataContext für einen Teilbereich eines Fensters meines Wissens nicht, und prinzipiell falsch ist die ItemsSource für die Comboboxen auch nicht gesetzt - oder doch? Die Art, wie sich das unkorrigierte Programm verhält, ist ja von vornherein arg seltsam: nur beim Wegschalten vom Daten-UserControl und wieder Zurückkehren, und dann ist der vorher gewählte Satz intakt bis auf diese eine Combobox mit der String-Property als SelectedValue.

W
955 Beiträge seit 2010
vor 7 Jahren

Ich würde in dem Hauptobjekt die Referenzen nicht per String oder int verweisen sondern direkt die Objekte als Member einbinden:


public class MainItem : VMBase
{
    private int _id;
    private string _name;
    private double _price;
    private ILookup _iLookup;
    private SLookup _sLookup;

    public int Id {
        get { return _id; }
        set { SetProperty(ref _id, value); }
    }
    public string Name {
        get { return _name; }
        set { SetProperty(ref _name, value); }
    }
    public double Price
    {
        get { return _price; }
        set { SetProperty(ref _price, value); }
    }
    public ILookupId ILookup
    {
        get { return _iLookup; }
        set { SetProperty(ref _iLookup, value); }
    }
    public SLookup SLookupId
    {
        get { return _sLookup; }
        set { SetProperty(ref _sLookup, value); }
    }
}

Dann in den Comboboxen nicht per SelectedValue sondern per SelectedItem referenzieren. Du hast jetzt ein minimales Beispielprojekt und kannst das mal testen. Es ist sowieso besser zu den Children direkt navigieren zu können als jedesmal ein Lookup durchführen zu müssen.

B
bb1898 Themenstarter:in
112 Beiträge seit 2008
vor 7 Jahren

Die Variante mit den Objekten als Member funktioniert dann und nur dann, wenn ich im XAML den Extra-Kontext für den Detail-Grid weglasse; das heißt, sie funktioniert unter der gleichen Bedingung wie die Variante mit den IDs.

Mit dem separaten DataContext wird es aber interessant: dann sind nämlich nach dem Hin- und Herschalten beide Comboboxen leer, nicht nur eine. Beide Lookup-Member im CurrentMainItem sind dann null. Dazu vermute ich, dass in meinem ursprünglichen Beispiel der Integer-Schlüssel einfach deshalb nicht zu null wird, weil er das ja nicht kann - er ist ja kein nullable int.

In einem meiner WPF-Bücher habe ich den folgenden Hinweis gefunden:
"Greifen Sie auf ein ListBoxItem zu, enthält dieses das eigentliche Objekt im DataContext. Wird nun in der ListBox nach unten gescrollt, sodass das ListBoxItem nicht mehr sichtbar ist, wird es aufgrund der UI-Virtualisierung vom Visual Tree entfernt. Der DataContext ist dann ungültig." (Huber, Windows Presentation Foundation 4.5, S. 649).
Ersetze "nach unten gescrollt" durch "UserControl durch ein anderes ersetzt" und wir sind nah dran. Nur sehe ich auch mit dieser Information noch nicht, warum der Rückgriff auf den UserControl-DataContext via RelativeSource den entscheidenden Unterschied macht.

B
bb1898 Themenstarter:in
112 Beiträge seit 2008
vor 7 Jahren

Und noch eine Variante mit etwas Code-behind: Der Detail-Grid bekommt wieder CurrentMainItem als DataContext; die beiden Comboboxen werden benannt und bekommen ihre ItemsSource im Code-behind zugewiesen. Da ich nicht sicher bin, ob die Zuweisung im Konstruktor evtl. zu früh kommen könnte (bevor der DataContext für das ganze UserControl gesetzt ist), habe ich eine Methode UserControl_Loaded dafür eingeführt.

Da bleiben die Werte in den Comboboxen bzw. die Lookup-Objekte im Datensatz beim Hin- und Zurückschalten erhalten.