Laden...

Forenbeiträge von bb1898 Ingesamt 112 Beiträge

11.04.2024 - 20:28 Uhr

Danke für die Hinweise! Ich habe sie angesehen und mir die Adressen notiert, mehr aber noch nicht. Ausprobieren wird Zeit brauchen und wahrscheinlich habe ich da noch einiges F# dazu zu lernen. Mal sehen, ob sich weitere Fragen ergeben oder ob ich zurechtkomme.

09.04.2024 - 16:39 Uhr

Ich würde gern WPF-Anwendungen schreiben, die kein (oder wenig) C# enthalten, sondern F# für die eigentliche Arbeit verwenden. Und ich habe Schwierigkeiten, dafür Informationen zu finden. Ich bin auf ElmishWPF gestoßen, aber das scheint wohl schon länger nicht mehr weiter entwickelt zu werden. Jedenfalls schien mir das entsprechende NuGet-Package mit meiner .NET-Version (.NET 8.0 als neuestes und standardmäßig vorgegebenes, in Visual Studio 2022) nicht zusammenzupassen.

Ein F#-Forum kann ich nicht finden, die Fragen auf StackOverflow waren überwiegend recht alt und haben auch sonst nicht gut gepasst.

Noch was: es geht um Programme, die ich selbst benutze (und niemand sonst), und die eher klein sind. Meist mit Anbindung an ebenfalls kleine Datenbanken.

20.02.2018 - 13:02 Uhr

Nochmalige Suche in der EF-Dokumentation hat wohl die entscheidende Stelle zutage gefördert:

Note that DbSet and IDbSet always create queries against the database and will always involve a round trip to the database even if the entities returned already exist in the context. A query is executed against the database when:

It is enumerated by a foreach (C#) or For Each (Visual Basic) statement.  
It is enumerated by a collection operation such as ToArray, ToDictionary, or ToListenter link description here.  
LINQ operators such as First or Any are specified in the outermost part of the query.  
The following methods are called: the Load extension method on a DbSet, DbEntityEntry.Reload, and Database.ExecuteSqlCommand.  

When results are returned from the database, objects that do not exist in the context are attached to the context. If an object is already in the context, the existing object is returned (the current and original values of the object's properties in the entry are not overwritten with database values). Finding entities using a query

Das dürfte der Knackpunkt sein. Es widerspricht im Übrigen auch meinem alten Lehrbuch nicht, wird dort halt nur nicht erwähnt (an der Stelle liegt der Fokus auf dem Vermeiden unnötiger Datenbankabfragen).

So dass ich also eine der von @Marco_GR aufgezählten Varianten brauche (Danke schön!). Damit scheint mir das Problem gelöst, ich habe den Thread als erledigt markiert.

18.02.2018 - 20:35 Uhr

Sending raw commands to the database

Note that any changes made to data in the database using ExecuteSqlCommand are opaque to the context until entities are loaded or reloaded from the database.
Ein Kontext kann einfach nicht das tracken, was Du an ihm vorbei schleust.

Das ist mir nicht neu, aber:

So far you’ve used LINQ to query a DbSet directly, which always results in a SQL query being sent to the database to load the data. Quelle: Lerman, Julia; Miller, Rowan: Programming Entity Framework : DbContext. O'Reilly Media, 2012. Kap. 2: Querying with DbContext; Querying Local Data. Blöderweise finde ich in meiner EBook-Ausgabe keine Seitenzahl dafür.

Daraus schließe ich doch, dass die Abfrage nach der Ausführung des SQL-Kommandos die Entities eben gerade neu aus der Datenbank lädt.

Warum Du einen ORM verwendest, um dann SQL Code Raw zu senden, erschließt sich mir nicht.
Das Verhalten ist - so wie ich das sehen - genau das.

So weit ich sehen kann, ist das die einzige Möglichkeit, aus dem EF heraus die Ausführung einer gespeicherten Prozedur anzustoßen. Und an einer Stelle in meinem Programm soll eben das stattfinden.

18.02.2018 - 12:06 Uhr

verwendetes Datenbanksystem: PostgreSQL 10.2, .NET-Provider Npgsql

Hallo,

ich möchte in einem WPF-Programm folgendes machen:

  • In einem DataGrid werden Datensätze angezeigt.
  • Auf Knopfdruck wird in der zugrundeliegenden Datenbank eine gespeicherte Prozedur ausgeführt, die Felder in eben diesen Datensätzen ändert.
  • Die Datensätze werden mit einer neuen Abfrage wieder aus der Datenbank geholt und im gleichen DataGrid angezeigt.

Für den ganzen Vorgang wird eine Instanz der passenden von DbContext abgeleiteten Klasse benutzt, die beim Öffnen des Fensters erzeugt und beim Schließen zerstört wird.

Das funktioniert so nicht: nach der zweiten Abfrage werden im DataGrid die unveränderten Daten angezeigt. Ich kann mich aber davon überzeugen, dass die gespeicherte Prozedur richtig ausgeführt wurde, z.B. indem ich das Fenster schließe und neu öffne.

Also habe ich ein kleines Konsolenprogramm gebastelt, das etwas Ähnliches tut:

  • Abfrage an die Datenbank, Ergebnisse anzeigen
  • Direkter UPDATE-Befehl an die Datenbank (context.Database.ExecuteSqlCommand(...))
  • Wiederholung von Abfrage und Ergebnisanzeige

Tut richtig, wenn die drei Aktionen in getrennten DbContext-Instanzen ablaufen; ich nehme sehr stark an, dass ich die ersten beiden auch zusammenfassen könnte, habe das aber noch nicht probiert. Läuft alles im gleichen Kontext ab, dann hat die zweite Abfrage das gleiche Ergebnis wie die erste.

Fragen, die ich mit Hilfe der Dokumentation nicht lösen konnte:

  • Was genau passiert da eigentlich? Ich lese immer und überall, dass Abfragen an den DbContext an die Datenbank geschickt werden - wenn man es anders haben will, muss man das extra einrichten. Auch wenn der SQL-Befehl in seiner eigenen Transaktion ausgeführt wird, müsste eine Abfrage, die danach abgesetzt wird, doch das Ergebnis schon kennen? Oder wird vielleicht der SQL-Befehl ohne mein ausdrückliches Zutun in einem eigenen Thread ausgeführt und das "danach" stimmt so eben nicht?

  • Ist eine neue DbContext-Instanz das Mittel der Wahl oder geht es auch anders?

Ich hänge das Konsolenprogramm mal an, obwohl der Beitrag sowieso schon lang ist. Es benutzt eine PostgreSQL-Version der Northwind-Datenbank, daraus nur drei Felder der Tabelle "Customers". Die zwei getrennten Namensräume sind hier natürlich ein schlechter Witz.

Danke für Hinweise aller Art!

#######################################################################

//Customer.cs
using System;

namespace Nordwind_Daten
{
    public class Customer
    {
        public string CustomerId { get; set; }
        public string CompanyName { get; set; }
        public string Region { get; set; }

        public override string ToString() => String.Format("Customer Id {0}, Company {1}, Region {2}",
                CustomerId, CompanyName, Region);
    }
}

//CustomerMap.cs
using System.Data.Entity.ModelConfiguration;

namespace Nordwind_Daten
{
    internal class CustomerMap : EntityTypeConfiguration<Customer>
    {
        public CustomerMap()
        {
            ToTable("customers");
            Property(c => c.CustomerId).HasColumnName("customerid")
                .IsFixedLength().HasMaxLength(5).IsRequired();
            Property(c => c.CompanyName).HasColumnName("companyname")
                .HasMaxLength(40).IsRequired();
            Property(c => c.Region).HasColumnName("region").HasMaxLength(15);
        }
    }
}

//NordwindDB.cs
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace Nordwind_Daten
{
    public class NordwindDB : DbContext
    {
        static NordwindDB()
        {
            Database.SetInitializer<NordwindDB>(null);
        }

        public NordwindDB() : base("name=NWConn")
        { }

        public DbSet<Customer> CustomerSet { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
            modelBuilder.HasDefaultSchema("public");
            modelBuilder.Configurations.Add(new CustomerMap());
        }

    }
}

//Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Npgsql;
using NpgsqlTypes;
using Nordwind_Daten;

namespace ExecSql_Nordwind
{
    class Program
    {
        static string GetCustomersByIdStart(string idstart, NordwindDB CDB)
        {
            var sb = new StringBuilder();
            //using (var CDB = new NordwindDB())
            //{
                var query = from cust in CDB.CustomerSet
                            where cust.CustomerId.StartsWith(idstart)
                            orderby cust.CustomerId
                            select cust;
                foreach (var cust in query)
                {
                    sb.AppendLine(cust.ToString());
                }
            //}
            return sb.ToString();
        }

        static int ChangeRegion(string idstart, string newchar, NordwindDB CDB)
        {
            string sql = "UPDATE customers SET region = CONCAT(region, :newchar) " +
                "WHERE customerid LIKE :idstart";
            //using (var CDB = new NordwindDB())
            //{
                var par_nc = new NpgsqlParameter(":newchar", NpgsqlDbType.Char, 1);
                var par_ids = new NpgsqlParameter(":idstart", NpgsqlDbType.Char, 2);
                par_nc.Value = newchar;
                par_ids.Value = idstart + "%";
                int cnt = CDB.Database.ExecuteSqlCommand(sql, par_nc, par_ids);
                return cnt;
            //}
        }

        static void Main(string[] args)
        {
            string newchar = "*";
            string appchar = "C";
            using (var CDB = new NordwindDB())
            {
                Console.WriteLine(GetCustomersByIdStart(appchar, CDB));
                int z = ChangeRegion(appchar, newchar, CDB);
                Console.WriteLine("Ergebnis von ChangeRegion: {0}", z);
                Console.WriteLine(GetCustomersByIdStart(appchar, CDB));
            }
            Console.Write("Beenden mit beliebiger Taste ...");
            Console.ReadKey();
        }
    }
}

05.07.2017 - 11:51 Uhr

Das ging ja fix! Danke an alle Antwortenden.

bzgl. der Exception. Dies liegt daran, daß bei der ObservableCollection<T> die virtuelle Methode InsertItem überschrieben ist

Und daher führt ((List)Items).AddRange(ships) keine PropertyChanged/CollectionChanged-Ereignisse aus, welches wiederum für die Konsistenz nötig sind.

Danke für die Klärung! Dann habe ich also die Wahl zwischen einer Schleife mit Add-Aufrufen und einer neuen Instanz. Na schön.

@T-Virus: Ich habe die eigene Subklasse ShipsCollection nur erstellt, um an die Items-Eigenschaft heranzukommen, die ist ja protected. Aber nachdem das nicht tut, was ich mir davon erhofft habe, ist die Klasse nicht mehr nötig.

05.07.2017 - 11:11 Uhr

Aufgabe: Der Inhalt einer ObservableCollection soll auf Kommando komplett ausgetauscht werden (meist als Ergebnis einer Suche in einem größeren Datenbestand).

Das funktioniert, wenn die neuen Objekte der Liste einzeln zugefügt werden:

public void Refill(IEnumerable<Ship> ships)
{
	Clear();
	foreach (var ship in ships)
	{
		Add(ship);
	}
}

Eigentlich läge es doch aber nahe, die neuen Objekte in einem Rutsch zuzufügen, mit AddRange oder so. Die ObservableCollection hat allerdings keine Methode, die das tut, ich habe in der Dokumentation jedenfalls nichts dergleichen gefunden. Versucht habe ich dies hier:

public class ShipsCollection : ObservableCollection<Ship>
{
	public ShipsCollection() : base() { }

	public ShipsCollection(IEnumerable<Ship> ships) => Refill(ships);

	public void Refill(IEnumerable<Ship> ships)
	{
		Clear();
		((List)Items).AddRange(ships);
	}
}

Das wird zwar anstandslos kompiliert, das Programm startet auch und das Ergebnis der ersten Suche wird richtig angezeigt. So bald aber eine neue Suche ein anderes Ergebnis hat, gibt es eine Exception. Weil der interessante Teil des Textes dazu recht lang ist, stelle ich erst mal meine Frage und hänge ihn danach an:

Stimmt meine Vermutung, dass so ein Komplett-Austausch der Elemente einer ObservableCollection nicht möglich ist oder habe ich nur den richtigen Weg nicht gefunden?

Klar ist, dass ich eine neue Instanz der Collection mit neuem Inhalt erzeugen könnte, aber dann müsste ich INotifyPropertyChanged auf die Collection als Ganzes anwenden. Und wäre das überhaupt eine gute Idee?

Und hier der gekürzte Text zur Exception:

Fehlermeldung:
System.InvalidOperationException ist aufgetreten.
HResult=0x80131509
Nachricht = Ein ItemsControl ist nicht konsistent mit seiner Elementquelle.
Weitere Informationen finden Sie in der inneren Ausnahme.
Quelle = <Die Ausnahmequelle kann nicht ausgewertet werden.>
...
Die Ausnahme wurde ausgelöst, da der Generator für Steuerelement 'System.Windows.Controls.DataGrid Items.Count:5' mit dem Namen '(unbenannt)' eine Reihe von CollectionChanged-Ereignissen empfangen hat, die nicht mit dem aktuellen Status der Elementsammlung übereinstimmen. Die folgenden Unterschiede wurden festgestellt:
Gesammelte Anzahl 1 unterscheidet sich von der tatsächlichen Anzahl 5. [Gesammelte Anzahl ist (Anzahl bei letztem Reset + #Adds - #Removes seit letztem Reset).]

Eine oder mehrere der folgenden Quellen haben möglicherweise falsche Ereignisse ausgelöst:
System.Windows.Controls.ItemContainerGenerator
System.Windows.Controls.ItemCollection
System.Windows.Data.ListCollectionView

  • ShipsCrud.ShipsCollection  
    

(Die beteiligten Quellen werden als die wahrscheinlichere Ursache des Problems betrachtet.)

Die häufigsten Ursachen umfassen (a) das Ändern der Sammlung oder deren Anzahl ohne Auslösen eines entsprechenden Ereignisses sowie (b) das Auslösen eines Ereignisses mit falschem Index- oder Elementparameter.

30.06.2017 - 15:52 Uhr

Aber dass deine beiden Queries im Startpost syntaktische Fehler haben, hast du gelesen?

Vielleicht bin ich vollkommen blind, aber ich verstehe nicht, wieso es beim ersten query funktioniert, beim zweiten nicht^^

Da würde ich mich jetzt gern anhängen: ich dachte eigentlich, ich wüsste, was an den beiden Abfragen auf genau die gleiche Weise falsch ist, aber warum Du bei der ersten richtige Ergebnisse (oder überhaupt ein Ergebnis) bekommen hast, bleibt mir ein Rätsel.

Der "DB Browser for SQLite" teilt mir bei einer vergleichbaren Fehlkonstruktion sofort mit, dass hier ein Syntaxfehler vorliegt. Ebenso sqlite3.exe. Dasselbe würde ich eigentlich von jedem derartigen Programm erwarten.

30.01.2017 - 17:51 Uhr

Ich weiß, der Thread ist nicht mehr druckfrisch, aber im Hinblick auf spätere Leser:
Du übergibst in keinem Fall eine Methode als Parameter; die hätte einen ganz anderen Datentyp. Du übergibst jedesmal das Ergebnis eines Methodenaufrufs, nur gibst Du dem einmal einen eigenen Namen, einmal nicht. Die Vorteile des ersten Vorgehens wurden schon genannt.

08.09.2016 - 20:53 Uhr

Wenn das Feld Id heissen würde, würde das EF automatisch erkennen, dass es sich hier um den Key handelt und ein Auto-Increment erfolgt.
PersonId erkennt das Ef nicht automatisch.

Das würde mich in der Klasse Person doch arg wundern. Die von Dir genannte Quelle sagt jedenfalls bei den Konventionen (Hervorhebung von mir):

The default convention for primary key is that Code-First would create a primary key for a property if the property name is Id or <class name>Id (NOT case sensitive). (Entity Framework Code First Conventions

08.09.2016 - 20:43 Uhr

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.

08.09.2016 - 20:28 Uhr

Wieso eine zweite Combobox, wenn es dort für jeden Gegenstand der ersten Combobox nur einen Eintrag gibt, also nichts zu wählen? Warum nicht einfach so:


<Combobox ItemsSource={Binding Gegenstandsliste}
          SelectedItem={Binding AktuellerGegenstand} />
<TextBox Text={Binding AktuellerGegenstand.Gewicht} />

Wenn das Gewicht nicht verändert werden soll, tut's auch ein TextBlock statt der TextBox.

Oder entspricht der "Gegenstand" in der ersten Combobox genau genommen einer Gruppe von Gegenständen mit verschiedenem Gewicht? Das könnte dann zum Beispiel so gehen:


<ComboBox ItemsSource={Binding Gegenstandsgruppenliste}
          SelectedItem={Binding AktuelleGegenstandsgruppe} />
<ComboBox ItemsSource={Binding AktuelleGegenstandsgruppe}
          DisplayMemberPath="Gewicht"
          SelectedItem={Binding AktuellerGegenstand} />

AktuellerGegenstand enthält in diesem zweiten Fall den Gegenstand mit dem gewählten Gewicht.

07.09.2016 - 20:00 Uhr

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.

05.09.2016 - 20:01 Uhr

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.

05.09.2016 - 12:49 Uhr

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!

03.09.2016 - 20:47 Uhr

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>

09.08.2016 - 17:11 Uhr

Geändert hat sich inzwischen jedenfalls die Server-Angabe für LocalDB, die muss jetzt so heißen:

"Server=(LocalDB)\MSSQLLocalDB" (auf Groß- und Kleinschreibung kommt es hier allerdings nicht an).

Kann das das Problem sein?

03.08.2016 - 17:22 Uhr

Bei solchen Fragen lohnt ein Blick in den
>
. GetDefaultView initialisiert das DataBinding, im Gegensatz zum einfachen Konstruktor.

Danke für den Hinweis, habe ich mir gleich mal lesegezeichnet (gelesezeichnet?).

Daneben finde ich sie praktisch für Master-Detail-Darstellungen: beim Wechsel des aktuellen Satzes in der Master-Liste soll die Detail-Liste mit den passenden neuen Sätzen gefüllt werden. Die ViewCollection kennt ihr CurrentItem und hat ein CurrentChanged-Ereignis, die zugrundeliegende Liste hat nichts dergleichen.

Während ich schreibe, kommt mir der Gedanke, dass sich das wahrscheinlich auch direkt im View regeln lässt, wird halt nur die Bindung der Detail-Liste etwas komplizierter. Muss ich mal ausprobieren.

Erfolgreich ausprobiert und ist in der Summe für ein halbwegs gewöhnliches Master-Detail-Szenario sogar eher kürzer, weil die diversen CurrentChanged-Handler entfallen.

19.07.2016 - 19:28 Uhr

In einer bestimmten Situation, beschrieben in meinem ersten Beitrag, hat Variante 2 nicht funktioniert. In anderen Situationen schon. M.E. gehört der Unterschied der beiden Konstruktionen in der Dokumentation erklärt, aber ich finde nichts.

13.07.2016 - 19:40 Uhr

Den Abschnitt aus der Dokumentation kenne ich schon, aber der besagt ja eigentlich nur, dass es die Default View immer gibt und wie man sie kriegt. Im Prinzip kann man aber beides tun: auf die automatisch erzeugte Default View-Komponente zugreifen und mit der arbeiten oder statt dessen mit einer eigens erzeugten CollectionView-Komponente arbeiten; der muss man natürlich die richtige Collection im Konstruktor übergeben. Oder hinterher die SourceCollection-Property setzen. Wenn ich mich nicht ganz falsch erinnere, wird die Default CollectionView nicht erzeugt, wenn man selbst eine erzeugt (so dass "es gibt sie immer" doch nicht ganz stimmt).

Wenn man mehrere Sortierungen oder Filter haben will, dann geht m.W. nur die Variante mit entsprechend vielen selbst erzeugten CollectionViews. Es finden sich andererseits auch Beispiele, in denen die Default View der ItemsSource einer Listbox o.ä. benutzt wird:


ListCollectionView view = CollectionViewSource.GetDefaultView(lstProducts.ItemsSource) as
    ListCollectionView;

Kommt aus einem nicht mehr ganz taufrischen WPF-Lehrbuch. Hat aber natürlich mit MVVM schon gar nichts zu tun. Bei so einem Verfahren ist die GetDefaultView-Variante mindestens naheliegend, wenn nicht zwingend, aber die Konstruktion ist eh nicht schön.

12.07.2016 - 20:44 Uhr

Da liegt wirklich ein Missverständnis vor: Tabellen- und Feldnamen einerseits, Feldwerte andererseits sind zwei Paar Stiefel und können nicht gleich behandelt werden. Tabellen- und Feldnamen sind im Normalfall ja Konstanten und können einfach in den SQL-Text geschrieben werden. Sind es doch Variable, hilft nur erst ordentlich überprüfen (falls vom Benutzer gekommen) und dann in den Text einbauen. Bei letzterem würde ich eher zu string.format("xxx {0}", variable) tendieren, aber das geht jetzt in die Geschmacksfragen.

Mit SQL-Parametern kann und soll man Feldwerte übergeben, sonst nichts. In Deinem funktionierenden Beispiel wird ja auch genau so verfahren.

12.07.2016 - 20:21 Uhr

Kannst du etwas zu den Gründen sagen?
In allen Fällen in denen ich mit einer ViewCollection gearbeitet habe, hat mehr Probleme als Vorteile mit sich gebracht. Ich habe dann auch immer einen anderen Weg gefunden meine Probleme zu lösen.

Wichtigster Grund dürften wohl die Fälle sein, in denen die gleiche Liste unterschiedlich sortiert oder gefiltert angezeigt werden soll - also mehrere CollectionViews für eine Collection.

Daneben finde ich sie praktisch für Master-Detail-Darstellungen: beim Wechsel des aktuellen Satzes in der Master-Liste soll die Detail-Liste mit den passenden neuen Sätzen gefüllt werden. Die ViewCollection kennt ihr CurrentItem und hat ein CurrentChanged-Ereignis, die zugrundeliegende Liste hat nichts dergleichen.

Während ich schreibe, kommt mir der Gedanke, dass sich das wahrscheinlich auch direkt im View regeln lässt, wird halt nur die Bindung der Detail-Liste etwas komplizierter. Muss ich mal ausprobieren.

Dein Betrag ist jetzt allerdings der erste, den ich sehe, der vom Arbeiten mit CollectionViews dezidiert abrät, und das mit "mehr Probleme als Vorteile" kann ich aus eigener Kenntnis nicht bestätigen.

Meine ursprüngliche Frage bleibt offen.

10.07.2016 - 16:11 Uhr

Ich werde jetzt mit den Parametern anfangen

Da tust Du auf jeden Fall mal gut daran, das brauchst Du nämlich für Datenbankanwendungen immer und dringend, ob nun mit Access oder mit einem anständigen Datenbanksystem.

Mein Lieblingszitat hierzu:

Warning

Never, never, NEVER use Python string concatenation (+) or string parameters interpolation (%) to pass variables to a SQL query string. Not even at gunpoint.

Ersetze "Python string concatenation (+) or string parameters interpolation (%)" durch "String-Manipulationsmethoden Deiner bevorzugten Programmiersprache".

10.07.2016 - 13:41 Uhr

Ausgangssituation: ich habe in meinen Daten irgendeine Sorte von Liste, will in einem WPF-Fenster daran binden und habe Gründe, im ViewModel nicht direkt auf die Liste zuzugreifen, sondern auf eine zugehörige CollectionView-Property.

Dann kann ich im Prinzip beides machen:

public class MainViewModel 
{
	public ObservableCollection<Zeugs> _meinKram;
	
	public ListCollectionView MeinKram { get; private set; }
	
	public MainViewModel()
	{
		_meinKram = HoleMeineDaten(irgend, welche, parameter);
		// Variante 1:
		MeinKram = (ListCollectionView)CollectionViewSource.GetDefaultView(
			_meinKram);
		// oder Variante 2:
		// MeinKram = new ListCollectionView(_meinKram);
		// ... alles, was sonst noch nötig ist
	}
	
	private ObservableCollection<Zeugs> HoleMeineDaten(parameter):
	{
		// irgendetwas
	}
	
	// ...
}

Ich finde im Netz und in meinen Lehrbüchern keine Erklärung zur Wahl zwischen beiden Varianten. Variante 1 scheint öfter benutzt zu werden, Variante 2 kommt aber auch vor.

Meine eigenen Experimente haben bisher eine Situation ergeben, in der nur Variante 1 das gewünschte Ergebnis hat - und da verstehe ich gar nicht, warum:

Die Daten enthalten eine Nachschlagetabelle, nennen wir sie Rubriken, und eine Datentabelle, nennen wir sie Dinge. Jedes Ding gehört zu genau einer Rubrik. In der Anwendung werden nur Dinge bearbeitet, hinzugefügt, evtl. gelöscht, die Rubriktabelle bleibt unverändert.

Ein Fenster hat drei Bereiche: Eine Listbox, einen Suchbereich, einen Detailbereich zur Bearbeitung eines Ding-Datensatzes.

Der Suchbereich hat u.a. eine Combobox zur Suche nach allen Dingen einer Rubrik. Der Detailbereich hat auch eine Combobox für Rubriken, hier soll dem einzelnen Satz die richtige Rubrik zugeordnet werden (diese Zuordnung kann sich schon mal ändern und muss jedenfalls für neue Sätze gesetzt werden). Die Listbox dient zur Anzeige des Suchergebnisses und zur Auswahl eines Satzes daraus.

Beide Comboboxen haben die gleiche ListCollectionView-Property des ViewModels als ItemsSource. In MainWindow.xaml sieht das so aus:

Im Suchbereich:

<ComboBox Grid.Row="1" Grid.Column="1" Margin="3"
		  ItemsSource="{Binding AlleRubrikenView}"
		  DisplayMemberPath="RubName"
		  IsSynchronizedWithCurrentItem="True" />

Im Detailbereich (das ist ein Grid, der den aktuellen Ding-Datensatz als eigenen DataContext hat):

<ComboBox Grid.Row="2" Grid.Column="1" Margin="3"
		  ItemsSource="{Binding DataContext.AlleRubrikenView, 
				RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
		  DisplayMemberPath="RubName"
		  SelectedValuePath="RubId"
		  SelectedValue="{Binding RubId}" />

Erzeuge ich AlleRubrikenView nach Variante 1 als DefaultView, dann verhält sich das Programm wie gewünscht, insbesondere sind die ausgewählten Rubriken in beiden Comboboxen unabhängig voneinander. Benutze ich Variante 2 und erzeuge AlleRubrikenView neu, dann ist in Suchbereich und Detailbereich immer die gleiche Rubrik ausgewählt, egal, in welchem Bereich ich eine Auswahl treffe. Das führt natürlich zu falschen Ergebnissen.

Ich würde es verstehen, wenn jede Bindung beider Comboboxen an die gleiche ListCollectionView-Property dazu führen würde, dass auch die Auswahl in beiden synchron verändert würde. Aber warum hängt das davon ab, wie die Property erzeugt wurde?

Und vor allem, wo wird das erklärt? In der offiziellen Dokumentation zu ListCollectionView und CollectionViewSource habe ich nichts gefunden, hier im Forum und auf StackOverflow auch nicht. Es ist natürlich immer möglich, dass ich einer Erklärung nicht alles entnommen habe, was drinsteckt. Oder aber falsch gesucht, und da kann mir hier vielleicht jemand mit weiteren Suchbegriffen unter die Arme greifen.

20.04.2016 - 17:26 Uhr

Hat sich erledigt, Zufallsfund in der Python-Mailingliste: mir war nicht klar, dass der Punkt "Standardwerte" im Kontextmenü der Eingabeaufforderung nicht etwa alles auf Standardwerte zurücksetzt, sondern mir erlaubt, die Voreinstellungen nach meinen Wünschen zu ändern. Das wirkt dann auch auf die Fenster, die Visual Studio beim Debuggen einer Konsolenanwendung öffnet.

19.04.2016 - 13:02 Uhr

Wenn ich aus Visual Studio heraus eine Konsolenanwendung starte, dann sieht das Fenster genau so aus wie gleich nach einer Windows-Installation: winzig, Schrift weiß auf schwarz und dünn. Auf meinem Desktop muss ich das zwar auch an vielen Stellen anpassen, das sind aber doch Einmalaktionen pro Installation.

Wenn ich zur Laufzeit der Anwendung, während sie auf eine Eingabe wartet, die Fenster-Eigenschaften ändere, dann überlebt diese Anpassung immerhin auch einen Neustart von Visual Studio, aber nur für dieses ganz bestimmte Projekt, nicht einmal für andere Projekte der gleichen Projektmappe.

Ich suche vergeblich nach einer Möglichkeit, die Eigenschaften des von der IDE gestarteten Konsolenfensters ein für alle Mal zu ändern. Unter "Extras / Optionen" kann ich nichts finden. In der Hilfe finde ich auch nichts, ich habe bei "Personalisieren der IDE" gesucht.

Kann mir jemand einen Tipp geben, wo ich die Information finden könnte?

22.03.2016 - 20:26 Uhr

Müssen dafür Stored Procedures gebaut werden?
Nein, SP ist immer die aller aller aller aller letzte Lösung.

Wieso denn das?

20.10.2015 - 19:52 Uhr

Zum Thema mit Xamarin. Mit deinem Tipp hat es geklappt! Über System.Text.StringBuilder lässt sich das Code ohne Probleme ausführen. Komisch, dass es anscheinend nicht korrekt referenziert wurde, so lange es aber so geht soll's mir egal sein 😃 Danke auch hierfür!

Der Namensraum System.Text ist nicht von allein eingebunden. Im Visual Studio enthält eine Klassendefinition normalerweise schon von Anfang an die entsprechende using-Anweisung im Kopf:

using System.Text;

Das scheint am Anfang Deiner Klassendatei nicht zu stehen und dann musst Du halt den StringBuilder komplett mit Namensraum ansprechen. Oder, natürlich, Du ergänzt die Anweisung.

20.10.2015 - 17:50 Uhr

Ich denke nicht das es sich jetzt schon auf mich auswirkt^^ aber besser schon mal auf dem aktuellen Stand sein als veraltet zu arbeiten^^

Ich habe die Iso gedownloadet, jedoch enthält sie zwei Ordner eine Datei und noch eine ".exe". Wie erstelle ich eine Iso? Ich habe nämlich die exe ausgeführt, gleiches Ergebnis...

Die Iso ist ja dafür gedacht, eine DVD daraus zu brennen. Wie das genau geht, hängt vom Brennprogramm ab. Bei Nero heißt es, wenn ich mich recht erinnere "Image brennen" o.ä.

08.10.2015 - 15:26 Uhr

Deine DetailseiteVM-Klasse muss natürlich auch PropertyChanged auslösen, sonst bekommt die GUI die Änderung nicht mit.

Genauer gesagt, die Eigenschaft AktuellesBuch in dieser Klasse braucht einen Setter, in dem PropertyChanged ausgelöst wird. Damit funktioniert es jetzt.

07.10.2015 - 17:55 Uhr

Spontane Frage ohne alles 2x gelesen zu haben :

public ListenanzeigeVM Listen { get; private set; }  
public DetailseiteVM Details { get; private set; }  

Die werfen kein RaisePropertyChanged ... warum genau nicht ?

Die werden im Konstruktor gesetzt und dann nie mehr ausgetauscht. Wenn sich ihre Eigenschaften ändern, ist dafür ja RaisePropertyChanged bei diesen Eigenschaften zuständig.

07.10.2015 - 11:52 Uhr

Meine Testanwendung bewegt sich von einem Problem zum nächsten: Keine Fehlermeldungen im Entwurfsmodus mehr, aber beim Umschalten zwischen meinen beiden TabItems findet keine Aktualisierung statt. Genauer: sie wird im Fenster nicht sichtbar.

Das Hauptfenster hat das ViewModel KrimiVM; es enthält zwei UserControls: Listenanzeige und Detailseite. Listenanzeige hat ViewModel ListenanzeigeVM, Detailseite hat ViewModel DetailseiteVM.

KrimiVM hat

public ListenanzeigeVM Listen { get; private set; }
public DetailseiteVM Details { get; private set; }

public KrimiVM()
{
	Listen = new ListenanzeigeVM();
	Details = new DetailseiteVM();
}

Das bisschen "Geschäftslogik", das die Anwendung kennt, findet in der Klasse KrimiBAPI statt. Die implementiert INotifyPropertyChanged und hat unter anderem die Eigenschaft AktuellesBuch:


public BuchVM AktuellesBuch
{
	get
	{
		return _aktuellesbuch;
	}
	set
	{
		if (value != _aktuellesbuch)
		{
			_aktuellesbuch = value;
			RaiseChange("AktuellesBuch");
		}
	}
}

BuchVM ist eine triviale Verpackung der Modellklasse Buch und implementiert ebenfalls INPC.

Der Zugriff auf KrimiBAPI erfolgt über die Anwendungsklasse:

public partial class App : Application
{
	private static KrimiBAPI _bapi = new KrimiBAPI();

	public static KrimiBAPI BAPI
	{ get { return _bapi; } }
}

Die Klasse DetailseiteVM greift nur lesend auf App.BAPI.AktuellesBuch zu:

public BuchVM AktuellesBuch
{ get { return App.BAPI.AktuellesBuch; } }

In der Klasse ListenanzeigeVM wird diese Eigenschaft beim Wechsel des selektierten Listbox-Eintrags neu gesetzt.

Ein Ablaufverfolgungspunkt in der Loaded-Methode der Detailseite zeigt: Im ViewModel ist die Eigenschaft richtig, d.h. beim Wechsel in der Liste und anschließendem Umschalten auf die Detailseite enthält sie das ausgewählte Buch. Trotzdem zeigt die Detailseite immer das Buch an, das beim ersten Umschalten auf diese Seite aktuell war.

Die Detailseite besteht aus einem DataTemplate für die Buch-Details und zwei Schaltern:

<UserControl x:Class="OkCancel_Versuche.Detailseite"
    ... (xmlns wie üblich)
    xmlns:local="clr-namespace:OkCancel_Versuche"
    ...
    Loaded="UserControl_Loaded">
    <UserControl.Resources>
        <DataTemplate DataType="{x:Type local:BuchVM}">
            <Grid>
               ... (Details für ein Buch)
            </Grid>
        </DataTemplate>
    </UserControl.Resources>
    <DockPanel>
        <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal">
          ... (Schaltflächen)
        </StackPanel>
        <ContentControl Margin="5" Content="{Binding Path=AktuellesBuch}" />
    </DockPanel>
</UserControl>

Im Hauptfenster werden die UserControls so eingebunden:

<Window.Resources>
	<DataTemplate DataType="{x:Type local:ListenanzeigeVM}">
		<local:Listenanzeige />
	</DataTemplate>
	<DataTemplate DataType="{x:Type local:DetailseiteVM}">
		<local:Detailseite />
	</DataTemplate>
</Window.Resources>
<Grid>
	...
	<TabControl Grid.Row="1" SelectedIndex="{Binding AktuelleSeite}">
		<TabItem Header="Liste">
			<ContentControl Content="{Binding Listen}" />
		</TabItem>
		<TabItem Header="Details">
			<ContentControl Content="{Binding Details}" />
		</TabItem>
	</TabControl>
</Grid>

Ich vermute oder bin eigentlich fast sicher, dass ich mit dem DataTemplate in der Detailseite etwas falsch mache, bloß was?

06.10.2015 - 15:55 Uhr

Der Konstruktor von KrimiBAPI:

public KrimiBAPI()
{
	KDB = new KrimiDB();
	KDB.Autoren.Load();
	AlleAutoren = new AutorlisteVM(KDB.Autoren.Local.OrderBy(au => au.Name));
	AktuellerAutor = AlleAutoren.FirstOrDefault();
	AutorBuecher = new BuecherlisteVM();
	if (AktuellerAutor != null)
	{
		AutorBuecher.RefillVM(AktuellerAutor.Buecher.OrderBy(b => b.Titel));
	}
}

Was möglicherweise nicht gut ist, ist meine Konstruktion in App.xaml.cs:

public partial class App : Application
{
	private static KrimiBAPI _bapi = new KrimiBAPI();

	public static KrimiBAPI BAPI
	{ get { return _bapi; } }
}

Überall, wo nötig, wird App.BAPI benutzt. Ich habe das aus einem WPF-Lehrbuch und bin damit bisher noch nicht in offensichtliche Probleme gelaufen.

Ich habe Änderungen gemacht, die die Fehlermeldung beseitigt haben, blicke aber nicht wirklich durch, warum das so ist. Ursprünglich war ein ViewModel für Hauptfenster und UserControls zuständig, jetzt habe ich es aufgeteilt und das Hauptfenster weiß gar nichts mehr vom Datenbankzugriff (sein ViewModel ebensowenig). Warum ich dann allerdings den Fehler nicht beim Bearbeiten der UserControls im Designer bekomme, bleibt mir unklar.

Ich habe jetzt allerdings ein neues Problem: mein Hauptfenster enthält ein TabControl mit zwei TabItems. Jedes TabItem wird ausgefüllt von einem UserControl mit eigenem ViewModel, beide ViewModels benutzen die in App.xaml.cs erzeugte Instanz von KrimiBAPI, die ihrerseits eine KrimiDB-Instanz enthält (und das ist der DbContext-Abkömmling). Ein UserControl zeigt den Inhalt einer Tabelle als Liste an, im anderen soll der gewählte Eintrag aus dieser Liste im Detail besichtigt und bearbeitet werden.

Der Fehler dabei: zwar wird der erste ausgewählte Eintrag auf der Detailseite richtig angezeigt, wähle ich auf der Listenseite aber etwas anderes aus, bleibt auf der Detailseite stur der erste Eintrag stehen. Egal, ob zwischen den zwei Selektionen der Inhalt der Liste einmal ganz ausgewechselt wird (Bücher eines anderen Autors) oder nicht.

Es wäre aber wahrscheinlich besser, aus der genaueren Beschreibung dieses neuen Problems auch einen neuen Thread zu machen, oder doch nicht?

05.10.2015 - 11:19 Uhr

verwendetes Datenbanksystem: MSSQL

Hallo zusammen,
ich habe eine Datenbankstruktur, in welcher ich eine Tabelle habe, in der ziemlich viele Daten optional sind.

Bist Du sicher, dass eine SQL-Datenbank für Deine Daten das Mittel der Wahl ist?

Daher habe ich beim ursprünlichen Design der Datenbank, die optionalen Daten in eine andere Tabelle ausgelagert. Es besteht zwischen den beiden Tabellen nun eine 1 zu {0,1} - Verknüpfung.

Nun ist es jedoch sehr nervig, die Tabellen immer zu joinen und ich überlege, ob ich die optionalen Daten nicht mit in die ursprüngliche Tabelle mit aufnehme. Zusätzlich denke ich, dass ich durch den nicht mehr benötigten Join vielleicht etwas Geschwindigkeit gewinnen könnte.

Andererseits handelst Du Dir zusätzliche Komplexität ein: Abfragen müssen die NULL-Werte berücksichtigen. Und sind bei einem Satz entweder alle optionalen Felder leer oder keins von ihnen? Dann muss jede Anwendung dafür sorgen, dass das immer richtig herüberkommt. Scheint mir potentiell schwieriger als wenn ein "optionaler Satz" grundsätzlich entweder erzeugt wird, mit korrekten Werten, oder ganz wegbleibt.

05.10.2015 - 11:03 Uhr

In einer kleinen Testanwendung mit WPF und Entity Framework bekomme ich diesen Fehler, sobald ich die .xaml-Datei für das Hauptfenster öffne:

Fehlermeldung:
InvalidOperationException: In der Anwendungskonfigurationsdatei wurde keine Verbindungszeichenfolge mit dem Namen 'KrimiCn' gefunden.
StackTrace
bei System.Data.Entity.Internal.LazyInternalConnection.get_ConnectionHasModel()
bei System.Data.Entity.Internal.LazyInternalContext.InitializeContext()
bei System.Data.Entity.Internal.InternalContext.GetEntitySetAndBaseTypeForType(Type entityType)
bei System.Data.Entity.Internal.Linq.InternalSet1.Initialize() bei System.Data.Entity.Internal.Linq.InternalSet1.GetEnumerator()
bei System.Data.Entity.Infrastructure.DbQuery`1.System.Collections.IEnumerable.GetEnumerator()
bei System.Data.Entity.QueryableExtensions.Load(IQueryable source)
bei OkCancel_Versuche.KrimiBAPI..ctor() in d:\home\Sibylle\Dokumente\Visual Studio 2012\Projects\OkCancel_Versuche\OkCancel_Versuche\ViewModel\KrimiBAPI.cs:Zeile 54.
bei OkCancel_Versuche.App..cctor() in d:\home\Sibylle\Dokumente\Visual Studio 2012\Projects\OkCancel_Versuche\OkCancel_Versuche\App.xaml.cs:Zeile 16.
InnerException: Keine

Das steht so im WPF-Designer-Fenster für MainWindow.xaml. Der xaml-Text sieht aus, wie er soll.

Trotzdem startet das Programm ganz normal und tut auch das Wenige, was es bisher kann und soll. Das heißt insbesondere, dass es Daten aus der Datenbank anzeigt.

Das Fenster enthält u.a. eine UserControl-Komponente. Zu den beiden gehören verschiedene ViewModels, die aber beide Referenzen auf die gleiche Instanz einer Klasse für die Geschäftslogik enthalten. Liegt hier das Problem?

Natürlich habe ich mir meine app.config gründlich angesehen, selbstverständlich steht die Verbindungszeichenfolge richtig drin und hat den Namen, unter dem das Programm auf sie zugreift.

Das sieht so aus:

public class KrimiDB : DbContext
{
	static KrimiDB()
	{
		Database.SetInitializer<KrimiDB>(null);
	}

	public KrimiDB()
		: base("name=KrimiCn")
	{ }
        
    ....
}

und in der app.config:

 <connectionStrings>
    <clear/>
    <add name="KrimiCn" providerName="System.Data.SqlClient"
         connectionString="data source=(localDb)\v11.0;initial catalog=Krimi_Versuch_1.KrimiDB;integrated security=True;" />
  </connectionStrings>

Es gibt auch nur diese eine app.config, die Projektmappe enthält nur ein Projekt, keine separaten Klassenbibliotheken. Die Datenbank ist in einem früheren Projekt erzeugt worden, das aktuelle Projekt heißt nicht "Krimi_Versuch_1".

Weder die Suche in diesem Forum noch die Suchmaschine meines Vertrauens haben weitergeholfen.

23.09.2015 - 10:31 Uhr

Jo, da scheint die Doku ausnahmsweise nicht ganz exakt zu sein.

Im Zweifelsfall richte dich aber lieber nach der Exception 😉

Da wird mir kaum etwas anderes übrigbleiben. Ein fauler Trick funktioniert:

if (!MyListCollectionView.CanCancelEdit)
{
    MyUndoMethod(mycurrentitem);
    MyListCollectionView.CommitEdit();     // Die Werte sind ja wieder die alten.
}
else
{
    MyListCollectionView.CancelEdit();
}

Aber besser ist es wohl doch, IEditableObject zu implementieren, notfalls mit einer leeren BeginEdit-Methode.

Bleibt die Frage, ob ich die ungenaue Dokumentation monieren werde.

22.09.2015 - 11:08 Uhr

dein snippet ruft so oder so

MyListCollectionView.CancelEdit();  

auf.
Dabei soll man das nicht aufrufen, wenn

.CanCancelEdit  

false ergibt.

Aber das sagt dir doch auch die Fehlermeldung.

Mein Punkt ist doch, dass die Dokumentation das so eben nicht sagt, sondern mindestens nahelegt, dass die Methode sehr wohl aufgerufen werden kann, dass eben nur die Wiederherstellung der alten Werte dann separat vorgenommen werden muss.

Die "Remarks" sagen eben gerade nicht dasselbe wie Du oben und deshalb widersprechen sich Dokumentation und Fehlermeldung.

21.09.2015 - 11:53 Uhr

Jedenfalls habe ich diesen Eindruck. Die Dokumentation zu dieser Methode sagt:

Ends the edit transaction, and if possible, restores the original value to the item.

Und unter "Remarks":

CancelEdit sets CurrentEditItem to null and causes the collection view to exit the edit state. If CanCancelEdit is true, CancelEdit also restores the original values of the edited object.

Daraus würde ich schließen, dass die Methode auch dann das Editieren beendet, wenn CanCancelEdit falsch ist. Sie kann dann nur nichts für das Wiederherstellen des editierten Objekts tun, das muss man selbst implementieren.

Das Folgende müsste also m.E. funktionieren:

if (!MyListCollectionView.CanCancelEdit)
{
	MyUndoMethod(mycurrentitem);
}
MyListCollectionView.CancelEdit();

Ich habe auch schon so ein Beispiel in der Dokumentation gesehen, kann es nur nicht wiederfinden. Tatsächlich bekomme ich aber eine Exception:

Fehlermeldung:
System.InvalidOperationException wurde nicht behandelt.
HResult=-2146233079
Message=CancelEdit wird für das aktuelle Bearbeitungselement nicht unterstützt.

Bevor ich mich in den Kampf mit dem Microsoft-Bugtracker begebe, frage ich lieber mal nach, ob ich da doch was missverstanden habe. Und ob hier eher ein Dokumentations- oder ein Programmfehler vorliegt.

Ich meine mich auch zu erinnern, dass die Methode früher mal so funktioniert hat, wie ich mir das vorstellen würde. Bloß finde ich das Beispiel nicht wieder, an dem ich das durchgespielt hatte.

14.09.2015 - 12:08 Uhr
  
if(x.HasValue)  
  

wäre dann mW best practice.

Was daran aber besser ist als


if (x != null)

bzw. für welche Fälle die erste Konstruktion besser ist, bleibt ungeklärt. Und ich denke, das war die eigentliche Frage oder nicht?
Am ehesten fällt mir noch ein, dass man x.Value in einer try-catch-Konstruktion verwenden kann, wenn man einen Grund hat, eine Exception der Abfrage vorzuziehen. Z.B. wenn x nur ausnahmsweise mal null ist. Oder wenn es um einen Wert geht, der sich zwischen Abfrage und Benutzung ändern könnte.

12.04.2015 - 12:49 Uhr

Steckt vielleicht die Klasse DatenBank in einem anderen Projekt und ist nicht ausdrücklich als public gekennzeichnet? Du zitierst leider nicht die genaue Fehlermeldung - in dieser Form sagt sie nicht mehr alles.

28.03.2015 - 15:07 Uhr

Hallo bb1898,

ein StackPanel ist immer so groß wie sein Inhalt. Da die ListBox verwendet intern ein ItemControl welches sich wie ein StackPanel verhält. Somit sieht es für die ListBox so aus als ob genügend Platz vorhanden ist und es werden keine ScrollBars angezeigt.

Das ist eine einleuchtende Erklärung, danke. Genau so etwas müsste aber doch auch in der offiziellen Dokumentation stehen. Fehlt es oder habe ich es bloß nicht gefunden oder evtl. eine vorhandene Erklärung nicht verstanden? Ich habe in der Zwischenzeit auch in drei verschiedenen WPF-Büchern nachgesehen, ebenso erfolglos.

25.03.2015 - 12:04 Uhr

Dieses Fenster tut nicht, wie es soll (und ja, diese Aussage werde ich weiter unten präzisieren):

<Window x:Class="Listen_Layout.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <StackPanel>
            <TextBlock Margin="10,5">Im StackPanel</TextBlock>
            <Border BorderBrush="DarkBlue" BorderThickness="3" Margin="10,5">
                <ListBox ItemsSource="{Binding AlleZeilen}" />
            </Border>
            <TextBlock Margin="10,5">Im StackPanel, Ende</TextBlock>
        </StackPanel>
        <DockPanel Grid.Column="1">
            <TextBlock DockPanel.Dock="Bottom" Margin="10,5">Im DockPanel, Ende</TextBlock>
            <TextBlock DockPanel.Dock="Top" Margin="10,5">Im DockPanel</TextBlock>
            <Border DockPanel.Dock="Top" BorderBrush="DarkRed" BorderThickness="3" Margin="10,5">
                <ListBox ItemsSource="{Binding AlleZeilen}" />
            </Border>
        </DockPanel>
    </Grid>
</Window>

Der Vollständigkeit halber noch die Klasse, aus der die Listen gefüllt werden:

class Listen_Daten
    {
        public ObservableCollection<string> AlleZeilen { get; private set; }

        public Listen_Daten()
        {
            AlleZeilen = new ObservableCollection<string>();
            for (int i = 1; i < 50; i++)
            {
                AlleZeilen.Add(String.Format("Listenelement Nr. {0}", i));
            }
        }
    }

Eine Instanz dieser Klasse bildet den DataContext für das Fenster.

Im DockPanel (rechte Fensterhälfte) sieht alles aus, wie es soll: Text oben, dann die Listbox mit einem vertikalen Scrollbalken, schön rot eingerahmt, Text unten.

Im StackPanel (linke Fensterhälfte) wird die Liste unvollständig angezeigt, ohne Scrollbalken, auch vom blauen Rahmen fehlt der untere Rand und der untere Text ist auch nicht da. Geht man mit der Cursortaste nach unten durch die Liste, sieht man nicht mehr, welcher Eintrag selektiert ist. Man kann das alles durch Vergrößern des Fensters herschaffen, aber das ist nicht der Witz von WPF.

Mein eigentliches Problem dabei: ich habe ein paar StackOverflow-Threads gefunden, in denen für solche Fälle zu einem anderen Layout-Container geraten wird. Ich habe aber nicht gefunden, wo in der Dokumentation diese Einschränkung des StackPanels beschrieben wird. Kann mir das jemand sagen?

Danke!

28.08.2014 - 16:17 Uhr

Ja, aber eben auf eine weniger umständliche Weise als mit dem Code, den ich bisher habe. Es geht also nicht um ein Ergebnis, das ich bekommen will und bisher nicht bekomme, sondern um eine bessere Methode, an eben dieses Ergebnis zu kommen.

Was ich beschrieben habe, ist nicht, wie ich es machen will, sondern wie ich es mache, weil mir nichts Besseres eingefallen ist.

27.08.2014 - 16:25 Uhr

Du sollst nicht erzählen wie du das was du vor hast in java gelöst hättest, sondern du sollst sagen was du als ergebnis haben willst.

Dein Weg ist etwas, sagen wir es vorsichtig, typisch Java, over-architected.

Wenn ich das auch nur annähernd richtig verstehe, hast Du meinen zweiten Beitrag ganz falsch verstanden: das sollte eine Beschreibung dessen sein, was der Beispiel-Code im ersten Beitrag tut (von Java war hier im übrigen nie die Rede), bevor er mir einen String liefert, der in eine Autorenliste deserialisiert werden kann. Das war der Versuch, zu erklären, warum ich ihn als umständlich und ein Hin-und-Her empfinde. Und was ich gern hätte, habe ich im übrigen auch gesagt.

27.08.2014 - 16:17 Uhr

Und wieso nicht so:

Buch myBook = JsonConvert.DeserializeObject< Book >( jsonString );  

Dann hast Du das Json, das ein Buch repräsentiert, in einer POCO (Entität) Klasse in einem Rutsch und feritg.
Ich versteh nicht, wieso Du so einen Aufwand machen willst 👶

Wenn das Json keine Entität darstellt, sondern nur Du diese erst zusammen bauen musst (zB wegen irgendwelchen ID-Referenzen), dann nimm mein Beispiel als DTO

BuchDTO myBookDTO = JsonConvert.DeserializeObject< BookDTO >( jsonString );  

Und arbeite dann managed mit .NET Objekten weiter um die Entität Book zu erstellen.
Aber sich mit JArray und JObject rum zu schlagen: wär mir zu unsicher, da untypisiert bzw auf Deutsch: zu doof 😉

Ich bin jetzt auf json2csharp gestoßen und habe das mal auf meinen Json-Text losgelassen. Da habe ich alle nötigen DTO-Klassen auf einen Sitz samt Root-Objekt, in das ich den kompletten Text deserialisieren kann. Das ist natürlich ganz nett, andererseits kriege ich auf diese Weise doppelt so viele Klassen wie ich eigentlich brauche, weil nur die Autor-Klasse direkt verwendet werden kann. Das muss man wohl je nach Anwendungsfall abwägen.

20.08.2014 - 12:58 Uhr

Ich versuche die Frage umzuformulieren, vielleicht wird es dann klarer: So, wie die Funktion augenblicklich ist, wandle ich zwar mit "DeserializeObject" den String mit den Autoren-Daten in einem Rutsch in die Liste um, die ich brauche, aber vorher habe ich die kompletten Daten in das JObject "alles" eingelesen,
daraus mit "SelectToken" den Teil mit den Autorendaten herausgeholt und in ein JToken gepackt, das namenlos bleibt,
dieses JToken in den String "autext" umgewandelt.

Und was ich gerne hätte: eine direkte Umwandlung des Ergebnisses von SelectToken in eine ICollection<Autor>, ohne ToString() zwischendrin. Dass dabei dieses Ergebnis als ein JArray interpretiert werden müsste, war eine Vermutung und ist nicht weiter wichtig.

19.08.2014 - 15:35 Uhr

Hallo,

ich habe eine JSON-Datei, die so aussieht:

{
  "Buch": [
    {
      "AutorName": "Sayers, Dorothy L.",
      "Titel": "Clouds of witness",
      "BuchID": 1,
      "Inhalt": "Diamantkatze, Parker begegnet Lady Mary"
    },
...
	{
      "AutorName": "Tremayne, Peter",
      "Titel": "Valley of the shadow",
      "BuchID": 32,
      "Inhalt": "33 tote junge M\u00e4nner, Ritualmord?"
    }
  ],
  "Autor": [
    {
      "Stamm": "Peter Wimsey, Charles Parker, Harriet Vane, Mervyn Bunter",
      "AutorName": "Sayers, Dorothy L.",
      "AutorID": 1
    },
...
	{
      "Stamm": "Fidelma von Cashel, Eadulf von Seaxmund's Ham",
      "AutorName": "Tremayne, Peter",
      "AutorID": 6
    }
  ]
}

Klassen für diese Daten habe ich auch. Um die Datei auszuwerten, benutze ich Newtonsoft.Json.NET, und da bin ich an einer Stelle nicht absolut zufrieden mit dem, was ich der Json.NET-Dokumentation bisher entnommen habe.

Was ich suche: ich möchte einen JArray in einem Stück in eine .NET-Collection überführen, ohne den Umweg über einen String zu gehen. Und dafür finde ich nichts in der Dokumentation.

Der Code, den ich bisher habe, sieht so aus:


static void KrimisAuswerten(string jsonname)
{
	JObject alles;
	using (StreamReader rdr = new StreamReader(jsonname))
	{
		JsonTextReader jsonrdr = new JsonTextReader(rdr);
		alles = (JObject)JToken.ReadFrom(jsonrdr);
	}

	// Die nächste Zeile missfällt mir, Kommentar s. unten
	string autext = alles.SelectToken("Autor").ToString();
	ICollection<Autor> autoren = JsonConvert.DeserializeObject<ICollection<Autor>>(autext);
	
	JArray buecher = (JArray)alles.SelectToken("Buch");
	List<Buch> AlleBuecher = new List<Buch>();
	foreach (dynamic buch in buecher)
	{
		Autor au = autoren.Single(a => a.AutorName == buch.AutorName.ToString());
		Buch neu = new Buch
		{
			BuchID = buch.BuchID,
			Titel = buch.Titel,
			Inhalt = buch.Inhalt,
			AutorID = au.AutorID,
			Autor = au
		};
		AlleBuecher.Add(neu);
	}
	foreach (Buch b in AlleBuecher)
	{
		Console.WriteLine(b.MitAutorToString());
	}
}

Was mir daran nicht gefällt: entweder ich muss meine Autorenliste, die ja schon Teil eines JObjects ist, in einen String zurückverwandeln, um sie dann mit DeserializeObject<ICollection<Autor>> als Ganzes in eine .NET-Collection umzuwandeln. Oder aber ich interpretiere sie gleich als JArray, wie ich es weiter unten mit der Bücherliste mache. Aber dann müsste ich jeden Satz einzeln in ein Autor-Objekt umwandeln und in meine Collection schreiben. Das ist bei der Bücherliste unproblematisch, weil ich da sowieso jeden Satz anfassen muss, um die AutorID zu ermitteln. Bei der Autorenliste wäre es unnötig.

Bin ich bloß nicht imstande, der Dokumentation ein besseres Vorgehen zu entnehmen, gibt es mehr Material zu Json.NET, das ich nicht gefunden habe, oder geht das nicht, was ich möchte?

16.07.2014 - 18:03 Uhr

Es sieht aus, als gehöre eine Entity im Zustand Added zwar zum Kontext, aber nicht zu ihrem zuständigen DbSet, obwohl sie genau diesem hinzugefügt wurde. Kann das so sein? Soll das so sein? Und wenn ja, wo ist es dokumentiert?

Jedenfalls mal im (elektronischen) Bücherschrank: Lerman, Julia & Miller, Rowan, Programming Entity Framework: DbContext. O'Reilly Media 2012.

Because a query is sent to the database to find the items in a DbSet, iterating a DbSet will only contain items that exist in the database. Any objects that are sitting in memory waiting to be saved to the database will not be returned. To ensure added objects are included you can use the techniques described in Querying Local Data.

Lesen bildet, aber nur immer wieder lesen hilft wirklich. In der MSDN-Dokumentation bin ich nicht auf diese Aussage gestoßen, aus welchen Gründen immer.

13.07.2014 - 12:03 Uhr

Ok, damit wäre mein Punkt (1) geklärt.

Es bleibt (2): warum werden in der Funktion ShowDetails die neuen Sätze nicht angezeigt? Dass sie noch keine ID haben, ist klar, sollte aber nichts ausmachen (bzw. es müsste 0 angezeigt werden). Ein Objekt wird einer Auflistung zugefügt, danach werden alle Elemente dieser Auflistung angezeigt und das neue Objekt ist nicht darunter. Was ist da los?

Was in dieser Vereinfachung nicht zu sehen ist: Normalerweise müsste ich ja frisch zugefügte Entities sofort als Navigationseigenschaften in Entities aus anderen Klassen benutzen können. Der Versuch führt zu einer Exception.

Das braucht eine Präzisierung: wenn ich mit der Entity selbst direkt weiter arbeite, klappt das schon. Nur wenn ich sie mit Hilfe eines geeigneten Suchkriteriums, hier DName, im DbSet suche, bekomme ich eine InvalidOperationException: die Sequenz enthält keine Elemente. Da dürfte aber die genaue Art der Exception davon abhängen, was genau ich mit dem Suchergebnis anfange, hier (mit dem eben vergebenen Wert für DName im String p):

           
 Detail f = context.Details.First(d => d.DName == p);

Es sieht aus, als gehöre eine Entity im Zustand Added zwar zum Kontext, aber nicht zu ihrem zuständigen DbSet, obwohl sie genau diesem hinzugefügt wurde. Kann das so sein? Soll das so sein? Und wenn ja, wo ist es dokumentiert?

12.07.2014 - 20:26 Uhr

verwendetes Datenbanksystem: LocalDB

Wenn ich einem DbSet ein neues Objekt zufüge, dann müsste es doch sofort, auch vor dem Speichern, in diesem DbSet zu finden sein; und so ist es auch bei meinen paar wenigen EF-Anwendungen bisher immer gewesen.

Jetzt auf einmal klappt das nicht mehr. Hier ist Test-Code, so primitiv es ging:


    public class Detail
    {
        public int DID { get; set; }
        public string DName { get; set; }

        public override string ToString()
        {
            return String.Format("{0} (ID = {1})", DName, DID);
        }
    }

    public class DetailMap : EntityTypeConfiguration<Detail>
    {
        public DetailMap()
        {
            HasKey(d => d.DID);
            Property(d => d.DName).IsRequired().HasMaxLength(50);
        }
    }

    public class Shopping : DbContext
    {
        public DbSet<Detail> Details { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
            modelBuilder.Configurations.Add(new DetailMap());
        }
    }

    class Program
    {
        static string ShowDetails(Shopping context)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("Bisher {0} Bedingungen\n", context.Details.Count());
            foreach (Detail d in context.Details)
            {
                sb.AppendLine(d.ToString());
            }
            return sb.ToString();
        }

        static void PrintPropertyValues(DbPropertyValues values)
        {
            foreach (var propertyName in values.PropertyNames)
            {
                Console.WriteLine(" - {0}: {1}", propertyName, values[propertyName]);
            }
        }

        static void PrintChangeTrackingInfo(Shopping _context, Detail entity)
        {
            var entry = _context.Entry<Detail>(entity);
            Console.WriteLine(entry.Entity.DName);
            Console.WriteLine("State: {0}", entry.State);
            Console.WriteLine("\nCurrent values:");
            PrintPropertyValues(entry.CurrentValues);
            Console.WriteLine("\nDatabase values:");
            // Exception: GetDatabaseValues cannot be used for entities in the Added state   <--- (1)
            // PrintPropertyValues(entry.GetDatabaseValues());
        }

        static void Main(string[] args)
        {
            using (Shopping context = new Shopping())
            {
                Detail neu = new Detail { DName = "XXX" };
                context.Details.Add(neu);
                PrintChangeTrackingInfo(context, neu);
                Console.WriteLine(ShowDetails(context));    <--- (2)
                context.SaveChanges();
            }
            Console.WriteLine("Weiter mit Enter ...");
            Console.ReadLine();
        }
    }
}


(1) Die Exception widerspricht m.E. der Dokumentation, in der es zu DbEntityEntry.GetDatabaseValues() heißt: "If the entity is not found in the database then null is returned."

(2) Hier werden nur die Detail-Sätze ausgegeben, die schon am Anfang in der Datenbank waren, der neue (mit DName XXX) fehlt und wird auch nicht mitgezählt.

Die EF-Version ist 6.1.1; es scheint eher nicht an dieser Version zu liegen. Ich habe in einem anderen Programm EF auch auf die neueste Version gebracht, ohne dass vergleichbare Probleme aufgetreten wären.

Was in dieser Vereinfachung nicht zu sehen ist: Normalerweise müsste ich ja frisch zugefügte Entities sofort als Navigationseigenschaften in Entities aus anderen Klassen benutzen können. Der Versuch führt zu einer Exception.

SaveChanges() speichert alles richtig.

Was mache ich da plötzlich falsch?