Laden...

Forenbeiträge von GeneVorph Ingesamt 180 Beiträge

22.12.2020 - 18:05 Uhr

Zunächst mal - vielen Dank, Abt! Das war jetzt mal verständlich erklärt: tatsächlich bin ich bei der ein oder anderen Sache von falschen Annahmen ausgegangen. Um nur eine zu nennen: ich hatte jetzt in diversen Videos, bzw. einem Blog mehrfach gesehen, wie Leute die mit SQLite arbeiten, sich einen Service auf einer static class aufbauen, weil ja der Zugriff eh nur "one at a time" ist. Bei genauerer Betrachtung macht es aber natürlich Sinn, nicht jedes Mal einen neuen Kontext zu erstellen.

Der Fehler selbst liegt meistens an einer ungültigen Relation oder, dass die Entität im Context selbst ungültig ist.

Meine Vermutung ist jetzt aber doch eher - beim Schreiben dieses Posts - dass die Entität "unsauber" ist.

Kann das sein, dass Du versuchst eine Entität zu löschen, die so gar nicht in der Datenbank ist oder verändert wurde?

Gerade letzteres ist für mich schwer zu sagen (mit meinem Kenntnisstand): das Erstellen eines SchoolClassData-Objekts, bzw. das Speichern in der Datenbank funktioniert so, wie es sollte - zumindest, wenn wir hier rein von der formalen Funktionalität sprechen. Das heißt ja nicht zwangsläufig, dass es auch "richtig" gemacht wurde.

Ich habe mir jedenfalls die Struktur und den Datensatz in der Datenbank im SQLite-Studio angesehen - die nötigen Tables sind da, auch die Ids sind korrekt vergeben, bzw. korrekt attribuiert.

Ich werde jetzt erst mal in guter alter Pen&Paper-Manier meine Modelle durchgehen, um rauszufinden, ob da evtl. wirklich was falsch "verdrahtet" ist.

22.12.2020 - 14:28 Uhr

Du arbeitest mit EntityFrameworkCore 6? Dann scheinst Du in der Zukunft zu sein, denn die Version gibts erst in 6 Monaten 😉
Es gibt aktuell EF Core 5 oder EF 6.

Mit über 40 Lenzen bin ich eher ein Mann der Vergangenheit 😉 War ein Typeo'; es muss freilich EF Core 5 heißen.

Hinweis: Der DbContext ist dazu da, dass er übergreifend geshared wird.
Dass Du hier mit statischer Klassen arbeitest widerspricht zum einen natürlich völlig OOP und zum anderen dem Umgang vom DbContext.
Das wird Dir also sehr bald Probleme machen; soviel vorweg.

Danke für den Hinweis - auch wenn ich schon eine Zeit hier bin, bin ich immer noch ein Hobby-Coder, der sich aus hunderten von Quellen alles zusammenstoppelt. Weiß nicht, ob es hier passt, aber kannst du da kurz bissl ins Detail gehen?

Ich sehe bei Dir aber nirgends hier eine Relations-Konfiguration im Modell.
Das wird womöglich der Fehler sein.

OK - was genau bedeutet "Relations-Konfiguration im Modell"? Ich habe dich jetzt so verstanden, dass das Modell EFCore "zeigt", wie die DB konfiguriert sein soll, also: Klassenname + Endung Id = Primär-Key, Properties = Columns, Objekt-Verweise = neuer Table (Mediator-Table?) usw. Ist es das, was du meinst? Ich hatte jetzt nur das SchoolClass-Modell angegeben...

Einstweilen vielen Dank - ich werde meine Modells nochmal auf logische Fehler hin abklopfen; womöglich werden wirklich Relationen falsch gesetzt. Auch wenn das - für mich - in der Migration erstmal so aussieht, als ob EFCore das so umgesetzt hätte, wie es meiner Meinung nach sein sollte (aber die kann ja falsch sein).

Gruß
vorph

PS: Du bist Dir hoffentlich bewusst, dass die Migrationen in Sqlite nur begrenzt funktionieren, da Sqlite kein Schema Change unterstützt.

22.12.2020 - 12:59 Uhr

verwendetes Datenbanksystem: EntityFrameworkCore 6, SQLite

Hallo,

ich habe meine Datenbank mit Hilfe von EFCore per Code-First erstellt. Ich kann neue Einträge hinzufügen, bekomme beim Löschen aber folgende Fehlermeldung

Fehlermeldung:
System.InvalidOperationException: "The property 'SchoolClassData.SchoolClassDataId' has a temporary value while attempting to change the entity's state to 'Deleted'. Either set a permanent value explicitly, or ensure that the database is configured to generate values for this property."

Leider habe ich keine Ahnung, wie ich den temporären Wert in einen nicht temporären umwandeln könnte, bzw. wie ich die die Datenbank konfigurieren könnte (--> außer den im Folgenden beschriebenen Schritten, die ich unternommen habe).

Die Methode, in der der Fehler ausgelöst wird (in meinem DataService):


public static void DeleteSchoolClass(SchoolClassData _schoolClass)
        {           
            using (var context = new DataBaseContext())
            {
                //Die nun folgende Zeile löst den Fehler aus
                var entity = context.SchoolClasses.Remove(_schoolClass);

                entity.State = Microsoft.EntityFrameworkCore.EntityState.Deleted;

                context.SaveChanges();
            }
        }

Erst dachte ich, der Fehler sein einfach zu beheben - leider hatte ich mit den folgenden Methoden keinen Erfolg:

  • Datenbank mit SQLite-Studio verbunden und den Key-Constraint 'ON DELETE RESTRICTED' zu 'CASCADE' geändert
  • Key-Constraint zu 'DEFAULT VALUE' geändert
  • Key-Constraints entfernt

Hier mein Daten-Modell:


public class SchoolClassData : ISchoolClass
    {
        [Required]
        public int SchoolClassDataId { get; set; }

        [Required]
        public SchoolClassInternalData SchoolClassInternals { get; set; }
    }

Nachdem ich die Datenbank erstmalig erstellt hatte, hatte ich eine Migration vorgenommen, bzw. die Datenbank geupdated. Hier der Code:

Mein DataContext:


 public class DataBaseContext : DbContext
    {
        public DbSet<SchoolClassData> SchoolClasses { get; set; }
        public DbSet<SchoolClassInternalData> SchoolClassInternals { get; set; }
        public DbSet<StudentData> Students { get; set; }
        public DbSet<PersonalData> Personals { get; set; }
        public DbSet<AddressData> Addresses { get; set; }
        public DbSet<EducationalData> Educationals { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            //optionsBuilder.UseSqlite("Data Source=Test_lite.sqlite");
            optionsBuilder.UseSqlite(connectionString: "FileName =./Test_DataBase01.sqlite");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<SchoolClassData>().ToTable("SchoolClasses");

            modelBuilder.Entity<SchoolClassInternalData>().ToTable("SchoolClassInternals");

            modelBuilder.Entity<StudentData>().ToTable("Students");

            modelBuilder.Entity<PersonalData>().ToTable("Personals");

            modelBuilder.Entity<AddressData>().ToTable("Addresses");

            modelBuilder.Entity<EducationalData>().ToTable("Educationals");
        }
    }


Meine initiale Migration:


public partial class InitialState : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Addresses",
                columns: table => new
                {
                    AddressDataId = table.Column<int>(type: "INTEGER", nullable: false)
                        .Annotation("Sqlite:Autoincrement", true),
                    Street = table.Column<string>(type: "TEXT", maxLength: 128, nullable: true),
                    PostalCode = table.Column<string>(type: "TEXT", maxLength: 64, nullable: true),
                    City = table.Column<string>(type: "TEXT", maxLength: 64, nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Addresses", x => x.AddressDataId);
                });

            migrationBuilder.CreateTable(
                name: "Educationals",
                columns: table => new
                {
                    EducationalDataId = table.Column<int>(type: "INTEGER", nullable: false)
                        .Annotation("Sqlite:Autoincrement", true),
                    StudentType = table.Column<string>(type: "TEXT", maxLength: 64, nullable: true),
                    ClassLabel = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
                    ClassLevel = table.Column<string>(type: "TEXT", maxLength: 64, nullable: true),
                    EducationalProgram = table.Column<string>(type: "TEXT", maxLength: 64, nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Educationals", x => x.EducationalDataId);
                });

            migrationBuilder.CreateTable(
                name: "Personals",
                columns: table => new
                {
                    PersonalDataId = table.Column<int>(type: "INTEGER", nullable: false)
                        .Annotation("Sqlite:Autoincrement", true),
                    FirstName = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
                    LastName = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
                    FullName = table.Column<string>(type: "TEXT", maxLength: 128, nullable: true),
                    DateOfBirth = table.Column<string>(type: "varchar(15)", maxLength: 15, nullable: true),
                    Age = table.Column<int>(type: "INTEGER", nullable: false),
                    Gender = table.Column<string>(type: "TEXT", maxLength: 20, nullable: true),
                    PhoneNumber = table.Column<string>(type: "varchar(128)", maxLength: 128, nullable: true),
                    EMail = table.Column<string>(type: "TEXT", maxLength: 128, nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Personals", x => x.PersonalDataId);
                });

            migrationBuilder.CreateTable(
                name: "SchoolClassInternals",
                columns: table => new
                {
                    SchoolClassInternalDataId = table.Column<int>(type: "INTEGER", nullable: false)
                        .Annotation("Sqlite:Autoincrement", true),
                    SchoolClassLabel = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
                    SchoolClassLevel = table.Column<string>(type: "TEXT", maxLength: 64, nullable: true),
                    SchoolClassHead = table.Column<string>(type: "TEXT", maxLength: 64, nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_SchoolClassInternals", x => x.SchoolClassInternalDataId);
                });

            migrationBuilder.CreateTable(
                name: "SchoolClasses",
                columns: table => new
                {
                    SchoolClassDataId = table.Column<int>(type: "INTEGER", nullable: false)
                        .Annotation("Sqlite:Autoincrement", true),
                    SchoolClassInternalsSchoolClassInternalDataId = table.Column<int>(type: "INTEGER", nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_SchoolClasses", x => x.SchoolClassDataId);
                    table.ForeignKey(
                        name: "FK_SchoolClasses_SchoolClassInternals_SchoolClassInternalsSchoolClassInternalDataId",
                        column: x => x.SchoolClassInternalsSchoolClassInternalDataId,
                        principalTable: "SchoolClassInternals",
                        principalColumn: "SchoolClassInternalDataId",
                        onDelete: ReferentialAction.Restrict);
                });

            migrationBuilder.CreateTable(
                name: "Students",
                columns: table => new
                {
                    StudentDataId = table.Column<int>(type: "INTEGER", nullable: false)
                        .Annotation("Sqlite:Autoincrement", true),
                    StudentPersonalPersonalDataId = table.Column<int>(type: "INTEGER", nullable: true),
                    StudentAddressAddressDataId = table.Column<int>(type: "INTEGER", nullable: true),
                    StudentEducationalEducationalDataId = table.Column<int>(type: "INTEGER", nullable: true),
                    SchoolClassInternalDataId = table.Column<int>(type: "INTEGER", nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Students", x => x.StudentDataId);
                    table.ForeignKey(
                        name: "FK_Students_Addresses_StudentAddressAddressDataId",
                        column: x => x.StudentAddressAddressDataId,
                        principalTable: "Addresses",
                        principalColumn: "AddressDataId",
                        onDelete: ReferentialAction.Restrict);
                    table.ForeignKey(
                        name: "FK_Students_Educationals_StudentEducationalEducationalDataId",
                        column: x => x.StudentEducationalEducationalDataId,
                        principalTable: "Educationals",
                        principalColumn: "EducationalDataId",
                        onDelete: ReferentialAction.Restrict);
                    table.ForeignKey(
                        name: "FK_Students_Personals_StudentPersonalPersonalDataId",
                        column: x => x.StudentPersonalPersonalDataId,
                        principalTable: "Personals",
                        principalColumn: "PersonalDataId",
                        onDelete: ReferentialAction.Restrict);
                    table.ForeignKey(
                        name: "FK_Students_SchoolClassInternals_SchoolClassInternalDataId",
                        column: x => x.SchoolClassInternalDataId,
                        principalTable: "SchoolClassInternals",
                        principalColumn: "SchoolClassInternalDataId",
                        onDelete: ReferentialAction.Restrict);
                });

            migrationBuilder.CreateIndex(
                name: "IX_SchoolClasses_SchoolClassInternalsSchoolClassInternalDataId",
                table: "SchoolClasses",
                column: "SchoolClassInternalsSchoolClassInternalDataId");

            migrationBuilder.CreateIndex(
                name: "IX_Students_SchoolClassInternalDataId",
                table: "Students",
                column: "SchoolClassInternalDataId");

            migrationBuilder.CreateIndex(
                name: "IX_Students_StudentAddressAddressDataId",
                table: "Students",
                column: "StudentAddressAddressDataId");

            migrationBuilder.CreateIndex(
                name: "IX_Students_StudentEducationalEducationalDataId",
                table: "Students",
                column: "StudentEducationalEducationalDataId");

            migrationBuilder.CreateIndex(
                name: "IX_Students_StudentPersonalPersonalDataId",
                table: "Students",
                column: "StudentPersonalPersonalDataId");
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "SchoolClasses");

            migrationBuilder.DropTable(
                name: "Students");

            migrationBuilder.DropTable(
                name: "Addresses");

            migrationBuilder.DropTable(
                name: "Educationals");

            migrationBuilder.DropTable(
                name: "Personals");

            migrationBuilder.DropTable(
                name: "SchoolClassInternals");
        }
    }

Der Migration zu folge sollte SchoolClassDataId PrimaryKey sein, nicht nullable, und AutoIncrement true.

Kann der Fehler noch eine andere Ursache haben? Was genau bedeutet 'temporärer Key' und wie kann ich das beheben.
Vielen Dank im Voraus,

Gruß
vorph

23.11.2020 - 20:41 Uhr

Welche Version von EF Core benutzt du?

Edit:
Versuch auch mal deine Navigation Property nicht mit new() zu initialisieren.

Halt! Ich nehme alles zurück und behaupte das Gegenteil! Gendau DAS war scheinbar das Problem - ich hatte es nur beim ersten Mal mit der falschen Methode getestet!

Nächster Task: eine gescheite DataAccessLayer...

vielen Dank!

23.11.2020 - 20:17 Uhr

OK, erst Mal vielen Dank für eure Antworten! Bevor ich im Einzelnen antworte ganz kurz zum Problem:

  • ich konnte es nun "lösen", indem ich eine foreach-Schleife nutze. Mein Code sieht nun so aus:
public static List<SchoolClass> GetAllClasses()
        {
            List<SchoolClass> _entity = new List<SchoolClass>();

            using (var context = new DataBaseContext())
            {
                if (context.SchoolClasses.Any())
                {
                    _entity = context.SchoolClasses.Include(ci => ci.SchoolClassInternals).ToList();

                    foreach (SchoolClass sc in _entity)
                    {
                        sc.SchoolClassInternals = context.SchoolClassInternals.Where(s => s.SchoolClassInternalId == sc.SchoolClassId).FirstOrDefault();
                    }
                }

                return _entity;

            }

Durch die Include-Methode wird (bzw. wurde auch vorher schon) das SchoolClassInternals-Property hinzugefügt - nur wurden seine Property-Daten nicht geladen. Ich dachte, das wäre mit Include abgehakt...

Und damit kommen wir schon zu

du kannst keine frontend modelle in der datenbank nutzen
und sollst das auch nicht
[Artikel] Drei-Schichten-Architektur

Können schon - aber es leuchtet ein, dass es eine schlechte Idee ist. Wird in der nächsten Überarbeitung behoben.

Welche Version von EF Core benutzt du?

Edit:
Versuch auch mal deine Navigation Property nicht mit new() zu initialisieren.

Version 5.0.0 --> habe new() beim Navigation-Property entfernt (es ändert aber nichts am eigentlichen Problem).

Eher keine ObservableCollection benutzen.
Die braucht man sowieso nicht im DAL

Auch richtig - ich dachte mir schon, dass mein Code derzeit höchst unsexy ist 😉 Ich versuche gerade alles, was ich online finden konnte (z. B. auf YouTube von Kanälen wie AngelSix, IAMTimCorey) aber auch zwei Udemy-Kursen und sämtlichen Blogs irgendwie sinnvoll zusammen zu tragen. Leider ist es nicht ganz einfach und nicht in ein paar Monaten machbar. Das wird wohl Jahre dauern, bis da wirklich "Profi-Code" drauß wird.
Leider verwenden viele Beispiele kein MVVM, andere pfeifen (aus Gründen der Einfachheit) auf DAL, eben um "nur mal kurz was zu zeigen" und dann muss man sich das aus unzähligen anderen Quellen zusammenstoppeln.

Ja. Du solltest dir mal ein Einführungs-Tutorial von EF Core anschauen, bevor du loslegst. Da ist alles erklärt, was du hier versuchst.

Kennst du denn ein gutes? Ich habe eines auf Udemy gefunden v. Mosh Hamedami, der auch auf YouTube aktiv ist. Es ist natürlich nahezu unmöglich zu sagen ob es denn "gut" ist (dazu hätte ich ja andere noch im Vergleich sehen müssen). Seine Schwäche ist, dass relationale Datenbezüge halt nur angekrazt werden - da konnte ich bei Tim Corey mehr finden; der übt aber eher Kritik an EF und so versuche ich halt das Beste aus beiden Welten zu generieren.

Gruß vorph

23.11.2020 - 15:39 Uhr

verwendetes Datenbanksystem: EntityFrameworkCore / SQLite

Hallo,

Ich habe in meiner wpf-Anwendung folgende Models:


 public class SchoolClass : ISchoolClassData
    {
        [Required]
        public int SchoolClassId { get; set; }

        [Required]
        public SchoolClassInternal SchoolClassInternals { get; set; } = new SchoolClassInternal();
    }


 public class SchoolClassInternal : ISchoolClassInternal
    {
        [Required]
        public int SchoolClassInternalId { get; set;}

        [Required]
        [MaxLength(64)]
        public string SchoolClassLabel { get; set; }

        [MaxLength(64)]
        public string SchoolClassLevel { get; set; }

        [MaxLength(64)]
        public string SchoolClassHead { get; set; }


        public ObservableCollection<Student> ClassStudents { get; set; } = new ObservableCollection<Student>();
        
    }

Wenn ich ein SchoolClass-Objekt erstelle und es an die SQLite-Datenbank übergebe, dann finde ich die übergebenen Daten dort wieder. Beim Lesen der Daten jedoch sind die Properties von SchoolClassInternals leer. Meine Methode:


public static List<SchoolClass> GetAllClasses()
        {
            List<SchoolClass> _entity = new List<SchoolClass>();

            using (var context = new DataBaseContext())
            {
                if (context.SchoolClasses.Any())
                {
                    _entity = context.SchoolClasses
                                                                 .Include(c => SchoolClassInternals)
                                                                 .ToList();
                }

                return _entity;

            }

Es werden zwar die richtigen SchoolClass-Objekte zurückgegeben, deren SchoolClassInternals-Properties sind jedoch leer (SchoolClassInternalId, SchoolClassInternalLabel, SchoolClassInternalLevel, SchoolClassInternalHead).

Ich habe mal den UP-Part der Migration kopiert - soweit ich das interpretieren kann, wurden alle Indizes richtig zugeordnet

 protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Adresses",
                columns: table => new
                {
                    AdressId = table.Column<int>(type: "INTEGER", nullable: false)
                        .Annotation("Sqlite:Autoincrement", true),
                    Street = table.Column<string>(type: "TEXT", maxLength: 128, nullable: true),
                    PostalCode = table.Column<string>(type: "varchar(12)", maxLength: 10, nullable: true),
                    City = table.Column<string>(type: "TEXT", maxLength: 128, nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Adresses", x => x.AdressId);
                });

            migrationBuilder.CreateTable(
                name: "Educationals",
                columns: table => new
                {
                    EducationalId = table.Column<int>(type: "INTEGER", nullable: false)
                        .Annotation("Sqlite:Autoincrement", true),
                    ClassLabel = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
                    ClassLevel = table.Column<string>(type: "TEXT", maxLength: 64, nullable: true),
                    EducationalProgram = table.Column<string>(type: "TEXT", maxLength: 64, nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Educationals", x => x.EducationalId);
                });

            migrationBuilder.CreateTable(
                name: "Personals",
                columns: table => new
                {
                    PersonalId = table.Column<int>(type: "INTEGER", nullable: false)
                        .Annotation("Sqlite:Autoincrement", true),
                    FirstName = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
                    LastName = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
                    FullName = table.Column<string>(type: "TEXT", maxLength: 128, nullable: true),
                    DateOfBirth = table.Column<string>(type: "varchar(15)", maxLength: 15, nullable: true),
                    Gender = table.Column<string>(type: "TEXT", maxLength: 20, nullable: true),
                    PhoneNumber = table.Column<string>(type: "varchar(128)", maxLength: 128, nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Personals", x => x.PersonalId);
                });

            migrationBuilder.CreateTable(
                name: "SchoolClassInternals",
                columns: table => new
                {
                    SchoolClassInternalId = table.Column<int>(type: "INTEGER", nullable: false)
                        .Annotation("Sqlite:Autoincrement", true),
                    SchoolClassLabel = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
                    SchoolClassLevel = table.Column<string>(type: "TEXT", maxLength: 64, nullable: true),
                    SchoolClassHead = table.Column<string>(type: "TEXT", maxLength: 64, nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_SchoolClassInternals", x => x.SchoolClassInternalId);
                });

            migrationBuilder.CreateTable(
                name: "SchoolClasses",
                columns: table => new
                {
                    SchoolClassId = table.Column<int>(type: "INTEGER", nullable: false)
                        .Annotation("Sqlite:Autoincrement", true),
                    SchoolClassInternalsSchoolClassInternalId = table.Column<int>(type: "INTEGER", nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_SchoolClasses", x => x.SchoolClassId);
                    table.ForeignKey(
                        name: "FK_SchoolClasses_SchoolClassInternals_SchoolClassInternalsSchoolClassInternalId",
                        column: x => x.SchoolClassInternalsSchoolClassInternalId,
                        principalTable: "SchoolClassInternals",
                        principalColumn: "SchoolClassInternalId",
                        onDelete: ReferentialAction.Restrict);
                });

            migrationBuilder.CreateTable(
                name: "Students",
                columns: table => new
                {
                    StudentId = table.Column<int>(type: "INTEGER", nullable: false)
                        .Annotation("Sqlite:Autoincrement", true),
                    StudentPersonalPersonalId = table.Column<int>(type: "INTEGER", nullable: true),
                    StudentAdressAdressId = table.Column<int>(type: "INTEGER", nullable: true),
                    StudentEducationalEducationalId = table.Column<int>(type: "INTEGER", nullable: true),
                    SchoolClassInternalId = table.Column<int>(type: "INTEGER", nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Students", x => x.StudentId);
                    table.ForeignKey(
                        name: "FK_Students_Adresses_StudentAdressAdressId",
                        column: x => x.StudentAdressAdressId,
                        principalTable: "Adresses",
                        principalColumn: "AdressId",
                        onDelete: ReferentialAction.Restrict);
                    table.ForeignKey(
                        name: "FK_Students_Educationals_StudentEducationalEducationalId",
                        column: x => x.StudentEducationalEducationalId,
                        principalTable: "Educationals",
                        principalColumn: "EducationalId",
                        onDelete: ReferentialAction.Restrict);
                    table.ForeignKey(
                        name: "FK_Students_Personals_StudentPersonalPersonalId",
                        column: x => x.StudentPersonalPersonalId,
                        principalTable: "Personals",
                        principalColumn: "PersonalId",
                        onDelete: ReferentialAction.Restrict);
                    table.ForeignKey(
                        name: "FK_Students_SchoolClassInternals_SchoolClassInternalId",
                        column: x => x.SchoolClassInternalId,
                        principalTable: "SchoolClassInternals",
                        principalColumn: "SchoolClassInternalId",
                        onDelete: ReferentialAction.Restrict);
                });

            migrationBuilder.CreateIndex(
                name: "IX_SchoolClasses_SchoolClassInternalsSchoolClassInternalId",
                table: "SchoolClasses",
                column: "SchoolClassInternalsSchoolClassInternalId");

            migrationBuilder.CreateIndex(
                name: "IX_Students_SchoolClassInternalId",
                table: "Students",
                column: "SchoolClassInternalId");

            migrationBuilder.CreateIndex(
                name: "IX_Students_StudentAdressAdressId",
                table: "Students",
                column: "StudentAdressAdressId");

            migrationBuilder.CreateIndex(
                name: "IX_Students_StudentEducationalEducationalId",
                table: "Students",
                column: "StudentEducationalEducationalId");

            migrationBuilder.CreateIndex(
                name: "IX_Students_StudentPersonalPersonalId",
                table: "Students",
                column: "StudentPersonalPersonalId");
        }

Habe ich etwas offensichtliches übersehen?

Gruß
Vorph

27.08.2020 - 20:34 Uhr

Du sollst das nicht in die Datenbank schreiben, sondern im ViewModel die Collection zum Lesen vorhalten die für den jeweiligen Typen vonnöten ist. Ein InstrumentViewModel bekommt im Konstruktor sein Modell injiziert und zusätzlich die Liste an Saitendingern die es zur Auswahl braucht - oder es filtert diese sich selbst heraus. Dazu ist das ViewModel schließlich da - das die Daten nicht so präsentiert werden müssen wie sie im Modell definiert wurden.

Verstehe - war'n Missverständnis.

Ich habe festgestellt, dass es auf StackOverflow ein paar Einträge dazu gibt, z. B. hier. Die Lösung schaut eigentlich ähnlich aus, wie das, was ich bereits erarbeitet hatte. Ich verstehe nur nicht, wo in meinem Code der Fehler liegt...

27.08.2020 - 16:49 Uhr

Du könntest alternativ den jeweiligen Saitensatz in der Instrument-Klasse halten von dem der Benutzer eines auswählt. .

Ja, im Prinzip schon - aber das hätte IMHO einen "Schönheitsfehler": damit müsste jedes Instrument mit allen möglichen Saitensätzen gefüllt werden, immer dann z. B., wenn ein Saitensatz neu hinzukommt; und der müsste dann bei allen Instrumenten in der Datenbank geupdated werden, wann immer dies der Fall wäre.

Insofern wäre es besser, wenn beim Befüllen des Grids einfach ein entsprechender Verweis anhand des InstrumentTypes auf die jeweilige ObservableCollection im ViewModel vollzogen würde.

Auch das Instrument- und StringSet-Object lassen sich viel besser verwalten, wenn sie nicht implizit miteinander verschränkt sind (will heißten b ist Property von a).
Trotzdem: vielen Dank für den Input, witte!

Jemand ne Idee zur Umsetzung des ursprünglichen Vorhabens?

26.08.2020 - 20:07 Uhr

Hallo,

ich stecke gerade bei folgendem Problem fest:

ich habe ein DataGrid, dessen ItemsSource eine ObservableCollection<Instrument> in meinem ViewModel ist.

Beim Typ 'Instrument' handelt es sich um Saiteninstrumente. 'Instrument' hat ein Property namens 'InstrumentType' (z. B. Gitarre, Bass, Ukulele...).

Nun möchte ich, dass im Datagrid für jeden Eintrag in meiner Collection eine Spalte mit einer ComboBox angezeigt wird, die verschiedene Saitensätze beinhaltet. Welche Saitensätze angezeigt werden, hängt von InstrumentType ab.

Um es einfach zu machen: so gibt es für den InstrumentType "AcousticGuitar", z. B. eine ObservableCollection<StringSet> AcousticGuitarStringSets, in der alle in Frage kommenden Saitensätze hinterlegt sind.

Konkret hänge ich dabei im XAML-Code - ich hatte an so etwas gedacht:


<DataGridTemplateColumn>
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <ComboBox>
                                    <Style TargetType="ComboBox">
                                        <Style.Triggers>
                                            <DataTrigger Binding="{Binding RelativeSource={RelativeSource Findancestor, AncestorType={x:Type Window}}, Path=DataContext.SelectedInstrument.InstrumentType}" Value="AcousticGuitar">
                                                <Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}, Path=DataContext.AcousticGuitarStringSets}"/>
                                            </DataTrigger>
                                            
                                        </Style.Triggers>
                                    </Style>
                                </ComboBox>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>

Ich muss dazu sagen, dass ich bei

<Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}, Path=DataContext.AcousticGuitarStringSets

vollkommen geraten habe. Für alles, was ich nach DataContext eingebe bekomme ich auch keine InteliSense-Vorschläge. Möglicherweise gehe ich das Problem falsch an.

Für Ratschläge schonmal vielen Dank!

Gruß
Vorph

06.07.2020 - 16:45 Uhr

Da sollte dir
>
helfen.

Danke! Das ist ein vielversprechender Ansatz - mal sehen, ob ich da was draus basteln kann...

06.07.2020 - 14:38 Uhr

Vielen Dank für die Vorschläge (und auch der Fehlerhinweis betreffs Typ meines Properties (string statt bool)).

Ich stelle fest, dass ich mein ursprüngliches Problem nicht gut genug durchdacht habe - und befinde mich gerade programmiertechnisch in einer Sackgasse.

Ich habe auf meinem Usercontrol einen Button, mit dem ich Einträge aus dem Datagrid löschen kann. Ich möchte aber, dass der Löschen-Button nur dann aktiv ist, wenn der User eine Reihe selektiert hat. Daher mein Versuch an den Selected-Event der DataGridRow heranzukommen. Denn - so die Idee - bei der Rückgabe true soll der CanExecute-Command-Handler den Button aktivieren, bei false deaktivieren (ausgegraut).

Mein Denkfehler: wenn ich eine Row anklicke und dann auf den Button klicke, wird die Row ja ebenfalls deselektiert (womit der Wert wieder false wäre) und mein Button wäre wieder ausgegraut.

Von der designtechnischen Seite ist es aber unbefriedigend, wenn man dem User keinen Hinweis geben kann, was er da gerade löscht. So wie es jetzt ist, klickt man die Row an und dann z. B. eine Textbox. Dabei ist das SelectedItem dann immer noch das Item der letzten Row, die man angeklickt hat - obwohl das DataGrid dann längst keinen Focus mehr hat. Nur, dass man es jetzt optisch nicht mehr wahrnimmt.

Irgendeine Idee, wie sich das vernünftig lösen ließe?

Und mal rein aus Neugier: das DataGridRow hat einen eigenen Satz an Events und Properties, über die Datagrid nicht verfügt. Wie kann ich diese über Interactions/Behavior ansprechen?

05.07.2020 - 01:21 Uhr

Hallo,

ich möchte in einem wpf-Projekt in dem ich ein DataGrid habe jeweils darauf reagieren, ob eine Zeile ausgewählt wurde oder nicht. Im ViewModel habe ich ein Property (IsRowSelected) vom Typ bool.

Dazu habe ich:


<DataGrid AutoGenerateColumns="False" SelectedItem="{Binding SelectedInstrument}" ItemsSource="{Binding AllInstruments}" SelectionMode="Single" Height="100">
     <DataGrid.RowStyle>
          <Style TargetType="{x:Type DataGridRow}">
              <Setter Property="IsSelected" Value="{Binding IsRowSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
           </Style>
     </DataGrid.RowStyle>

<DataGrid.Columns>
    <DataGridTemplateColumn Width="*">
         <DataGridTemplateColumn.Header>
             <TextBlock Text="Instrument"/>
         </DataGridTemplateColumn.Header>
          <DataGridTemplateColumn.CellTemplate>
              <DataTemplate>
                 <TextBlock Text="{Binding InstrumentLabel}"/>
              </DataTemplate>
          </DataGridTemplateColumn.CellTemplate>
<!--...-->
 </DataGridTemplateColumn>

Im ViewModel sollte IsRowSelected eigentlich 'true' sein, wenn ich eine Row im DataGrid anklicke, bzw. 'false', wenn ich ein anderes Steuerelement anklicke.


private string _isRowSelected;

        public string IsRowSelected
        {
            get { return _isRowSelected; }
            set 
            {
                OnPropertyChanged(ref _isRowSelected, value); 
            }
        }

Ich hatte im Setter einen Haltepunkt eingerichtet. Allerdings wird der überhaupt nicht angesprochen, wenn ich im DataGrid eine Row anklicke, also scheint hier etwas grundsätzlich schief zu laufen. Habe ich irgendwo einen offensichtlichen Fehler?

Gruß
vorph

21.03.2020 - 08:30 Uhr

In Button.IsPressed steht, daß durch ENTER diese Eigenschaft nicht gesetzt wird, aber generell das Click-Ereignis ausgelöst wird.

Hm...das erklärt einiges. Gleichzeitig verkompliziert es die Sache natürlich.

Spontan fällt mir nur ein, per Interactivity ein Command an das Click-Ereignis zu hängen und im ViewModel ein Property einzurichten, dass der View sagt, wann das Click-Ereignis eingetreten ist.
Klingt arg "gebastelt". Wahrscheinlich nicht so gut, oder?

Gruß
Vorph

20.03.2020 - 23:46 Uhr

Hallo,

Ich habe folgenden Code, um Text in TextBoxen zu markieren, sobald diese Focus bekommen (per Klick oder Tab) in meiner App.xaml.cs:


 public partial class App : Application
    {

        protected override void OnStartup(StartupEventArgs e)
        {
            EventManager.RegisterClassHandler(typeof(TextBox), UIElement.PreviewMouseLeftButtonDownEvent,
               new MouseButtonEventHandler(SelectivelyHandleMouseButton), true);
            EventManager.RegisterClassHandler(typeof(TextBox), UIElement.GotKeyboardFocusEvent,
              new RoutedEventHandler(SelectAllText), true);

            base.OnStartup(e);
        }

        private static void SelectivelyHandleMouseButton(object sender, MouseButtonEventArgs e)
        {
            var textbox = (sender as TextBox);
            if (textbox != null && !textbox.IsKeyboardFocusWithin)
            {
                if (e.OriginalSource.GetType().Name == "TextBoxView")
                {
                    e.Handled = true;
                    textbox.Focus();
                    textbox.SelectAll();
                }
            }
        }

        private static void SelectAllText(object sender, RoutedEventArgs e)
        {
            var textBox = e.OriginalSource as TextBox;
            if (textBox != null)
                textBox.Focus();
                textBox.SelectAll();
        }

    }

Mein XAML-Code:


<Window.Resources>
<Style x:Key="TextBoxFocus" TargetType="TextBox">
            <Style.Triggers>
                <DataTrigger Binding="{Binding ElementName=btnSave, Path=IsPressed}" Value="True">
                    <Setter Property="FocusManager.FocusedElement" Value="{Binding ElementName=txtName}"/>                    
                </DataTrigger>
</Style>
</Window.Resources>

Im Prinzip ist mein Vorhaben recht trivial: in txtName wird Text eingegeben. Klickt man nun auf den Button btnSave, wird der Text gespeichert und die Eingabe in txtName markiert (so dass man gleich die nächste Eingabe machen kann).

Jedenfalls: bei meinem Button btnSave ist IsDefault = "True". Benutze ich also statt der Maus die Entertaste, dann wird zwar mein Eintrag gespeichert, aber der Text in txtName wird nicht markiert. Außer, ich wechsle vorher den Focus auf ein anderes Control (egal, ob per Mouse oder Tab), z.B. zu einer anderen TextBox oder einer ComboBox, etc., dann funktioniert es.

Es hat den Anschein, als wäre es nicht möglich den Text zu markieren, wenn ich unmittelbar nach der Texteingabe Enter drücke. Was auch ganz logisch ist, denn ich habe ja beim Binding für den DataTrigger nur den Path "IsPressed" angegeben.

Gibt es ein Property, mit dem ich auf das Drücken der Enter-Taste reagieren kann? Mir wäre keins bekannt.

Hat jemand eine Idee, wie ich es dennoch mit der "Enter"-Taste bewerkstelligen kann, ohne vorher ein anderes Control in den Fokus rücken zu müssen?

Gruß
Vorph

01.03.2020 - 20:34 Uhr

Danke - das war in der Tat das Problem! Ich hatte jetzt so lange schon erfolglos hin und herprobiert, ich hätte das wahrscheinlich erst in ein paar Tagen bemerkt - wenn überhaupt^^

viele Grüße
Vorph

01.03.2020 - 11:55 Uhr

Hallo,

wie ich jetzt mehrfach gelesen habe, ist es nicht so ohne weiteres möglich die Visibility einer DataGridTemplateColumn /Textcolumn zu binden. Der Grund ist wohl darin zu suchen, dass - warum auch immer - die Columns nicht im VisualTree des DataGrids liegen. Damit ist dann ganz schnell klar, warum es auf alt hergebrachte Weise nicht funktionieren kann, da in diesem Fall auch kein Zugriff auf den DataContext mittels RelativeSource möglich ist. Soweit so gut.

Ich habe mehrere Lösungsvorschläge online gefunden, die sich im Prinzip alle auf zwei Lösungsansätze stützen:
a) ein Proxy-Objekt vom Typ FrameworkElement oder ContentControl, das den DataContext einer View bereitstellt und die Column-Visibility sozusagen "fernsteuert".

b) eine Relay-Klasse, die von Freezable erbt

Beide Ansätze können z. B. hier gefunden werden:
https://stackoverflow.com/questions/22073740/binding-visibility-for-datagridcolumn-in-wpf
https://www.technical-recipes.com/2017/binding-the-visibility-of-datagridcolumn-in-wpf-mvvm/

Ich versuche gerade Methode a) nachzuprogrammieren, kann aber beim besten Willen nicht feststellen, wo ich da einen Denkfehler(?) habe.

Hier mein Code:
MainWindow


<Window x:Class="VisualTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:vm="clr-namespace:VisualTest.ViewModels"
        xmlns:local="clr-namespace:VisualTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <Window.DataContext>
        <vm:MainViewModel/>
    </Window.DataContext>
    
    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BtVConverter"/>
    </Window.Resources>
    
    <Grid>
        <StackPanel>

            <FrameworkElement x:Name="dummyElement" Visibility="Collapsed"/>
            
            <DataGrid ItemsSource="{Binding TheSource}" AutoGenerateColumns="False">
                
                <DataGrid.Columns>
                    <DataGridTextColumn Header="Meine Objekte" Binding="{Binding Labeling}">
                        
                    </DataGridTextColumn>

                <!--Um diese Zeile geht es: das Binding funktioniert nicht! -->
                <DataGridTextColumn Header="Meine Werte" Binding="{Binding AValue}" Visibility="{Binding DataContext.IsVisible, Source={x:Reference dummyElement}}">
                <!-- Es macht hier keinen Unterschied ob ich DataContext.IsVisible habe, oder wie in einem Beispiel .IsEnable-->
                   
                </DataGridTextColumn>
                    
                </DataGrid.Columns>
                
            </DataGrid>
           
            <Rectangle Fill="Red" Width="20" Height="20" Visibility="{Binding IsVisible, Converter=   
                                                   {StaticResource BtVConverter}}"/>

            <CheckBox x:Name="ckbToggleVisibility" Content="Sichtbarkeit an/aus" Margin="20" IsChecked="{Binding IsVisible, Mode=OneWayToSource}">
                
            </CheckBox>

            <TextBlock Margin="30" Text="{Binding IsVisible, Mode=OneWay}">
             
            </TextBlock>
            
        </StackPanel>
    </Grid>
</Window>

Mein MainViewModel:


 public class MainViewModel : NotifyPropertyChangedBase
    {
        public ObservableCollection<MyObject> TheSource { get; set; } = new ObservableCollection<MyObject>();

        private bool _isVisible;
        public bool IsVisible
        {
            get { return _isVisible; }
            set
            {
                _isVisible = value;
                OnPropertyChanged(ref _isVisible, value);
            }
        }

        public MainViewModel()
        {
            MyObject o1 = new MyObject()
            {
                Labeling  = "Objekt 1",
                AValue = "Value 1"
            };

            MyObject o2 = new MyObject()
            {
                Labeling = "Objekt 2",
                AValue = "Value 2"
            };

            MyObject o3 = new MyObject()
            {
                Labeling = "Objekt 3",
                AValue = "Value 3"
            };

            MyObject o4 = new MyObject()
            {
                Labeling = "Objekt 4",
                AValue = "Value 4"
            };

            MyObject o5 = new MyObject()
            {
                Labeling = "Objekt 5",
                AValue = "Value 5"
            };

            MyObject o6 = new MyObject()
            {
                Labeling = "Objekt 6",
                AValue = "Value 6"
            };

            MyObject o7 = new MyObject()
            {
                Labeling = "Objekt 7",
                AValue = "Value 7"
            };

            TheSource.Add(o1);
            TheSource.Add(o2);
            TheSource.Add(o3);
            TheSource.Add(o4);
            TheSource.Add(o5);
            TheSource.Add(o6);
            TheSource.Add(o7);
        }

Mein Daten-Model (reiner Dummy, nur für Testzwecke)


public class MyObject
    {
        public string Labeling { get; set; }
        public string AValue { get; set; }
    }

Und nur der Vollständigkeit halber --> mein Codebehind


 public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }

Ich habe auf dem MainWindow eine Checkbox, deren IsChecked-Eigenschaft an ein Property des MainViewModels gebunden ist: IsVisible. Dieses ist vom Typ bool und soll eigentlich (bei geckeckter Checkbox) dafür sorgen, dass die Werte-Spalte im DataGrid auf "collapsed" gesetzt wird.

Unter dem DataGrid befindet sich ein rotes REctangle, das an die IsVisible-Eigenschat auf dem MainViewModel gebunden ist. Wird das Häkchen bei der Checkbox gesetzt, wird das Rectangle sichtbar.

Außerdem habe ich ein TextBlock, der an IsVisible gebunden ist, und den aktuellen Staus (true oder false) in Textform ausgibt.

Mein Problem: setze ich das Häkchen bei der Checkbox, wird zwar das Rectangle sichtbar, bzw. in der Textbox wird "true" angezeigt, die DataGridColumn bleibt aber unverändert.
An welcher Stelle in meinem Code habe ich einen Fehler?

viele Grüße
Vorph

29.02.2020 - 21:08 Uhr

Ich habe eine – nicht ganz einfache – aber MVVM-verträgliche Lösung für das Filtern von Datagrids gefunden. Ich poste hier meine Lösung, möchte jedoch gleich vorweg schicken, dass ich denke, dass jemand mit mehr Erfahrung mit Sicherheit eine elegantere und wahrscheinlich auch mit weniger Code behaftete Lösung finden wird. Betrachtet meine Lösung von daher vielleicht besser als „Ausgangspunkt“.

Der „Trick“ zur Lösung war eigentlich folgender: die meisten Lösungsvorschläge, die ich im Internet gefunden habe, bauen auf der Implementierung von ICollectionView auf, der ich im Konstruktor meines ViewModels die zu filternde Collection übergebe:


MyCollectionView = new ICollectionView.GetDefaultView(MyFilterCollection);

Fangt.So.Nicht.An! Dieser Ansatz ist eine Sackgasse, wenn es um mehr als einen Filter geht.

Wenn ich nur einen einzigen Filter auf das DataGrid anwenden möchte, funktioniert das soeinwandfrei. Mehrere Filter hingegen werden schnell problematisch, bzw. funktionieren nach meinem jetzigen Kenntnisstand gar nicht.

Mein Setting (so einfach wie möglich, so generisch wie nötig):
Ein Datagrid soll nach drei Filterkriterien gefiltert werden. Jedes Kriterium ist in einer extra TextBox. Der Einfachheit halber heißen meine Filterkriterien Item, Element und Value. Diese sind Typen, auf die ich weiter unten näher eingehe. Hier ist nur wichtig: jedes Item, Element und jeder Value haben ein Property ‚Name‘, so dass die ComboBox mit dem Item-Filter z. B. die Einträge
• Alle Items, Item1, Item2, Item3…
beinhalten könnte.

Analog dazu die ComboBoxes für Elemente und Values.
Zunächst habe ich drei ObservableCollections vom Typ Item, Element und Value (zu diesen Typen, s. unten), bezeichnet als ItemsList, ElementsList und ValuesList. Sie enthalten die Elemente, die als Filterkriterien in den ComboBoxen, die später zum Filtern des Datagrids benutzt werden, bereitgestellt werden.
Ich habe in meinem ViewModel außerdem eine ObservableCollection vom Typ „MyFilterType“ mit Namen ‚PrimaryFilter‘. Hier die Klasse MyFilterType:


public class MyFilterType
{
   public Item MyItem {get;set;}
   public Element MyElement {get;set;}
   public Value MyValue {get;set;}
}

Die Klassen für Item, Element und Value sind komplett simpel und sollen nur die zugrunde liegenden Prinzipien veranschaulichen. Da ihr Aufbau gleich ist, hier nur die Klasse Item:


public class Item
{
      public string ItemName {get;set;}
      public string ItemValue {get;set;}
}

Außerdem benötige ich ein CollectionViewSource-Property:


 private CollectionViewSource _filterView;
 public CollectionViewSource FilterView
 {
       get {return _filterView; }
       set   
            {
                 _filterView = value;
                 PropertyChange(nameof(_filterView);
            }
 }

Dieses kümmert sich um die Filterlogik – daher bekommt es im Konstruktor des ViewModels unsere Filter-Collection:


 FilterView.Source = PrimaryFilter;

Vorbereitungen im XAML:
Zunächst gebe ich als DataContext natürlich mein ViewModel an:


<Window.DataContext>
   <viewModels:MainViewModel/>
</Window.DataContext>
 

(Nur am Rande: ich lege meinen DataContext immer im XAML fest, prinzipiell sollte es aber auch MVVM-konform sein, dies im code behind zu tun. Die meisten Beispiele im www beziehen sich auf code behind. Um das mal kurz für Einsteiger in XAML zu demonstrieren:

  • Erstellt euch in eurem Projekt einen Ordner mit Namen ‚ViewModels‘
  • Erstellt in diesem Ordner euer ViewModel
  • Referenziert den Namespace im XAML der View:

 <Window x:Class="DataGrid_Templating.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
…
xmlns:viewModels ="clr-namespace:[hier der name deines Projekts ohne die eckigen Klammern].[Hier der Name deines Folders, z. B. „ViewModels“, ohne eckige Klammern]" 

)

Als nächstes erstellen wir ein DataGrid und drei ComboBoxes, die später die Auswahlmöglichkeiten für die Filter bereitstellen sollen. Für dieses Beispiel wird das so aussehen, dass die erste ComboBox nach „Items“ filtert, die zweit nach „Element“ und die dritte nach „Value“ (s. auch angefügtes Bild).

Dementsprechend sollte ComboBox1 mit den ItemNames befüllt sein, ComboBox2 mit den ElementNames und ComboBox3 mit den ValueNames. Da dies hier aber nicht Gegenstand meines Posts ist, überspringe ich das Binding hier.

Wichtig ist zunächst nur, dass in eurem ViewModel für jede ComboBox ein property zu Verfügung gestellt wird, das das aktuelle Filterkriterium bereithält. Da die Properties vom Aufbau her alle gleich sind, hier nur das für ComboBox1 wesentliche Property:


private string _currentItem;
public string CurrentItem
{
    get { return _currentItem; }
    set
          {
            _currentItem = value;

            PropertyChange(nameof(_currentItem));
          }
}

Im XAML der ComboBox bindet man nun die SelectedItem-Eigenschaft auf dieses Property:


<ComboBox x:Name="cmbItemFilter" ItemsSource="{Binding ItemsList}" SelectedIndex="0" SelectedItem="{Binding CurrentItem}">                    
</ComboBox>

Nun zurück zum ViewModel:
Hier benötigt man für jeden Filtertyp eine Filter-Methode. Die Methoden sind analog zueinander aufgebaut, deshalb beschränke ich mich hier auf die ItemFilter()-Methode:


public void ItemFilter()
{
    MyFilterView.Filter -= new FilterEventHandler(FilterByItems);
    MyFilterView.Filter += new FilterEventHandler(FilterByItems);
}

Der Code sieht nicht intuitiv aus – zuerst wird ein FilterEventHandler entfernt, dann wieder hinzugefügt. Manch einer wird sich fragen: „Was soll das?“.
Im Prinzip ist es einfach erklärt. Wenn ich den Handler setze, werden sämtliche Filterregeln auf meine Source angewandt (in diesem Fall PrimaryFilter). Der „Filterzustand“ der Collection bleibt dabei erhalten – wende ich nun einen zweiten Filter an, wird dieser auf den aktuellen, will heißen: vorgefilterten, Zustand der Collection angewandt. Das hätte zur Folge, dass ich z. B. beim ersten Filtern des DataGrids nach Item1 zwar Item1 angezeigt bekomme, dann aber nicht mehr nach Item2 filtern kann, weil die aktuelle Filterauswahl nur Item1 enthält. Dieses Verhalten umgeht man, indem man zunächst den alten Filter „zurücksetzt“ (FilterEventHandler ab-abonnieren) und dann direkt neu abonniere).

Fehlen nur noch die Methoden mit der eigentlichen Filterlogik. Ich habe dazu für jeden Filter eine Methode - bei drei Filtern, ergo drei Methoden. Da auch die vom Aufbau her gleich sind, beschränke ich mich wieder nur auf die Methode zum Filtern von Items (FilterByItems):


public void FilterByItems(object sender, FilterEventArgs e)
{
    var src = e.Item as Item;
  
    if (src == null)
    {
        e.Accepted = false;    
    }
    else if (CurrentItem == "Alle Items")
    {
       e.Accepted = true;
    }
    else if (string.Compare(CurrentItem, src.CurrentItem) != 0)
    {
       e.Accepted = false;           
    }
}

Was tut die Methode? Sie vergleicht den String im der aktuell in der für diesen Filter zuständigen ComboBox mit der zu filternden Collection. Alle Items, die den gleichen Namen haben wie das Item in der Filter-ComboBox werden zur Collection hinzugefügt (e.Accepted = true).

Fertig? Noch nicht ganz! Zwei Schritte noch, dann haben wir’s geschafft:

  1. Die betreffenden Filter müssen natürlich irgendwo aktiviert werden. Dazu geht man noch mal zu den Properties, die das jeweils aktuelle Item, Element, Value bereithalten und ändert sie so ab:

private string _currentItem;
public string CurrentItem
{
      get { return _currentItem; }
      set
      {
      _currentItem = value;

      if (CurrentItem == „Alle Items“)
      {
          ElementFilter();
          ValueFilter();
      }
      else
     {
          ItemFilter();
      }

           PropertyChange(nameof(_currentItem));
      }
}

Kleine Anmerkung: „Alle Items“ in der Auswahl wäre sozusagen eine Art „default“ (es soll überhaupt nicht nach Items gefiltert werden). Trifft dies zu, muss ich sozusagen die Zustände der anderen Filter „überprüfen“, d.h. ich rufe die betreffenden Filtermethoden auf. Hier ist wichtig sich in Erinnerung zu rufen, dass z. B. für CurrentElement das Property so aussehen würde:


private string _currentElement;
public string CurrentElement
{
     get { return _currentElement; }
     set
     {
           _currentElement = value;

           if (CurrentElement == „Alle Items“)
          {
             ItemFilter();
             ValueFilter();
          }
          else
          {
             ElementFilter();
           }

          PropertyChange(nameof(_currentItem));
     }
}

Ich rufe die betreffende Filtermethode also nur auf, wenn ein Item, Element, bzw. Value ausgewählt wurde, sonst immer die anderen Filtermethoden.

  1. DataGrid-Binding. Code sagt ja mehr als tausend Worte 😉 Aber: wichtig ist, nicht auf die Collection „PrimaryFilter“ zu binden, sonder auf die CollectionViewSource, also auf MyFilterView und hier (ganz wichtig!!!) auf das Property VIEW:

<DataGrid x:Name="dtgMainGrid" ItemsSource="{Binding MyFilterView.View}"> </DataGrid>

Wenn ihr die Anwendung jetzt startet, kann gefiltert werden 🙂
Viel Spaß dabei,
Gruß
Vorph

17.02.2020 - 14:23 Uhr

Ja, dein XAML war richtig - das "Binding" hatte ich gar nicht mehr bemerkt (auch so ein Effekt, wenn man stundenlang rumfurhwerkt); das muss natürlich weg und dann funktioniert es auch so, wie es sein soll.

Zu den Links:ausnahmsweise ist der microsoft.com-Artikel wirklich gut; die Sache mit den AutoGeneratedColumns habe ich jetzt einstweilen zurückgestellt, bis ich das mit dem Filtern besser verstanden habe (die Links aber gesichtet und gebookmarkt.
Ich hatte selbst bereits ein paar gefunden, die im Ansatz eine Erklärung geben - das Problem dabei war, dass ich erst jede Menge Refactoring betreiben musste, weil die Artikel aus Gründen der Einfachheit einfach alles in den codebehind klatschen.

Ich bin wieder zuversichtlich 😃 Also vielen Dank!

Gruß
Vorph

16.02.2020 - 22:35 Uhr

Vielen Dank, Th69!

PS: Du solltest selber lernen, im Internet passende Links zu finden (am besten gleich auf englisch suchen).

😁 Wenn du wüsstest, welch bitterböse Ironie damit verbunden ist! Ich habe zwei Tage zwischen vier und sechs Stunden nur Artikel gewälzt, die mich letzlich kaum weiterbrachten. Wie mal jemand so schön gesagt hat: "Programmieren lernt man alleine oder gar nicht!" Ich verstehe das immer besser: gerade bei WPF gibt es zig tausend Quellen und oft wird nur mal schnell was dahergezeigt, was mit MVVM nichts zu tun hat. Bleibt noch das Problem, dass ich oft "blindfischen" muss, weil ich gar nicht weiß, welchen Begriff ich nun suche^^ Aber: seit ein paar Monaten merke ich Vortschritte - es kann also nur besser werden 😁

15.02.2020 - 22:43 Uhr

OK, ich dachte mir schon, das das eine größere Baustelle wird.
Ich versuche mich gerade mit CollectionViewSource anzufreunden, bekomme aber auch einen einfachen Aufbau nicht hin. Hier mal mein Versuch:

Ich habe ein simples Datagrid auf dem MainWindow, ein MainViewModel und mein Student-Model.

MainWindow XAML


<Window x:Class="DataGrid_Templating.MainWindow"
      ...
        xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
       ...

    <Window.DataContext>
        <viewModels:MainViewModel/>
    </Window.DataContext>

    <Window.Resources>
        <CollectionViewSource x:Key="XFirstFilter" Source="{Binding FirstFilter}">
            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription PropertyName="Binding LastName" Direction="Ascending"/>
            </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>
    </Window.Resources>    
   ...
        <DataGrid Grid.Row="1" Grid.Column="1" AutoGenerateColumns="True" ItemsSource="{Binding Source={StaticResource XFirstFilter}}">     
        </DataGrid>
    </Grid>
</Window>

Hier mein MainViewModel:


 public class MainViewModel
    {
..
        public ObservableCollection<Student> FirstFilter { get; set; } = new ObservableCollection<Student>();
        
        public MainViewModel()
        {
            CreateSome();
            PopulateFilter();
        }

        private void PopulateFilter()
        {
            foreach(Student s in MyStudents)
            {
                Student f = new Student();

                f.LastName = s.LastName;
                f.PreName = s.PreName;
                f.ClassLabel = s.ClassLabel;
                f.EducationalType = s.EducationalType;               
                f.SubjectAverage = s.Subjects.AverageGrade;

                FirstFilter.Add(f);
            }
        }

        private void CreateSome()
        {
            Student s1 = new Student();
            s1.LastName = "Hubbes";
            ...

            Student s3 = new Student();
           
            MyStudents.Add(s1);
            MyStudents.Add(s2);
            MyStudents.Add(s3);
            MyStudents.Add(s4);

        }
    }

Soweit ich das verstanden habe, kann ich prüfen, ob soweit alles funktioniert, indem ich in der Windows.Resources die ViewCollection implementiere und



            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription PropertyName="Binding LastName" Direction="Ascending"/>
            </CollectionViewSource.SortDescriptions>

eine SortDescription hinzufüge. In meinem Fall binde ich auf das Property "LastName" und setze Direction = Ascending.
Leider passiert bei mir rein gar nichts: wenn ich die App starte, wird die Reihenfolge der Einträge unter LastName so dargestellt, wie sie der Collection hinzugefügt wurden. Erst wenn ich auf den Header klicke, werden sie sortiert (aber auch erst mal descending...). Also stimmt mein binding schon nicht. Nur: wo ist der Fehler?

Gruß
Vorph

15.02.2020 - 20:17 Uhr

Du bindest einfach zwei VM-Eigenschaften an die SelectedItem Eigenschaft der beiden ComboBoxen und in der an das DataGrid gebundenen Eigenschaft ObservableCollection<Student> Students wertest du die beiden anderen VM-Eigenschaften aus und gibst entsprechend eine gefilterte Liste zurück.

OK, das entspricht im Grunde mienem Ansatz. Die ersten beiden Links dazu sind leider etwas kurz - der Codeproject-Artikel thematisiert aber genau mein Anliegen.

Ich werde jetzt noch einiges zur Filterimplementierung und CollectionViewSource zu recherchieren haben; konnte dazu jetzt auch erst den Codeproject-Artikel überfliegen. Ein generelles Problem scheint mir aber erhalten zu bleiben:

  • je nach Setting der ComboBoxen werden bestimmte Inhalte angezeigt/nicht angezeigt. In meinem Fall heißt das: Für cB1 "Klasse A1" und cB2 "L" soll das DAtaGrid alle Schüler anzeigen, die zur Klasse A1 gehören und im Bildungsgang L sind. Die Logik dazu zu implementieren ist nicht schwer, ich frage mich derzeit nur, wie ich das DAtaGrid dazu bekomme mir die Daten so anzuzeigen, wie sie angezeigt werden sollen, da ich - sofern ich mit Templates arbeiten möchte - AutoGenerateColumns auf "false" setzen muss.

Das fängt bei den Headern schon an: ich bräuchte schon sinnvolle Namen, statt "PreName" eben 'Vorname", statt 'Art" 'Bildende Kunst" (Leerzeichen im String). Normalerweise handele ich das über Templates, allerdings (nach meinem Kenntnisstand) würde das bedeuten, dass ich AutoGenerateColumns auf "false" setzen muss, was wiederum bedeutet, dass ich mir bereits vor der Runtime überlegen müsste, wie mein DAtaGrid aussieht - was aber irgendwie doch der Idee von Filtern entgegengesetzt ist, weil je nach Usecase mein Gird anders strukturiert ist.

Oder offenbaren sich hier Defizite meinerseits, was das Templating von DataGrids angeht? Momentan bin ich nämlich auf dem Stand, das durch DataGridColumnTemplates z. B. jede einzelne Column, die später im DG enthalten sein wird, manuell festgelegt wird.

Danke nochmals für die Links,
Gruß
Vorph

15.02.2020 - 14:43 Uhr

Hallo,

vor der Frage ganz kurz zum Vorhaben:

ich möchte ein DataGrid erstellen, dessen Datendarstellung von zwei ComboBoxes abhängt. Diese sollen als Filter fungieren. Im Anhang dazu ein Bild, das den Sachverhalt deutlich zeigen dürfte.
Mein Model bildet ‚Students‘ ab (Name, Vorname, Klasse, Bildungsgang).

In meinem ViewModel befindet sich eine ObservableCollection<Student> DefaultFilter.
Binde ich die ItemsSource meines DataGrids auf diese Collection, erhalte ich vier Spalten (mit den Headern PreName, LastName, ClassLabel, EdcType) und die zugehörigen Daten. Soweit – so gut.

Jetzt zur Frage:
Im Bild seht ihr zwei ComboBoxes. In der linken, „Schüler“, gibt es die Auswahlmöglichkeit „Alle Schüler“, bzw. es besteht noch die Möglichkeit die Klassen A1 und B1 auszuwählen.
In der ComboBox rechts, „Bildungsgänge“ gibt es die Möglichkeit „Alle BGs“, sowie L, G und H.
Das DataGrid solle also in der Lage sein, die Daten je nach Auswahl in den ComboBoxen zu präsentieren (Etwa „Alle Schüler“ + „H“ oder „A1“ + „L“).

Wie sollte ich geschickterweise konzeptionell vorgehen?

Meine Idee dazu:
Ich erstelle eine Pre-Filter-Collection, in der alle Properties von Student abgebildet werden. Dann setze ich die ItemsSource des DataGrids im Code auf diese Collection. Allerdings dürfte das MVVM verletzen.

Daher könnte ich die Pre-Filter-Collection einfach anstelle des Default-Filters setzen:

DefaultFilter.Clear();
DefaultFilter = PreFilterCollection;

Mein Problem damit: Die Header im DataGrid zeigen immer den Propertynamen. Um hier sinnvolle Bezeichnungen zu haben, muss ich im XAML ein Template im DataGridTemplateColumn.Header anlegen; nur funktioniert dann meine Idee mit der Pre-Filter-Collection nicht mehr, da die Columns dann ja vordefiniert sind und nicht mehr dynamisch generiert werden.
Für alle Hinweise, wie ich mich dieser Aufgabe nähern kann, schon mal vielen Dank!

Gruß
Vorph

15.02.2020 - 11:51 Uhr

Hi, und vielen Dank für euere Antworeten!

@Mr.Sparkle

Was spricht dagegen, diese 10 Eigenschaften anstatt der Liste zum ViewModel hinzuzufügen?

Prinzipiell gar nichts - tatsächlich habe ich es jetzt erst einmal so gelöst. Ich nenne das jetzt mal eine "Mediator"-Klasse (kenne den Fachbegriff nicht); eine Klasse, die alle Verschachtelungen als einfache Properties bereitstellt.

@Th69 Nicht ganz ein Verständnisproblem; mir war ja schon klar, dass es so nicht funktioniert - es wäre aber durchaus denkbar gewesen, dass es da eine Art "Workaround" gibt. Übrigens: danke für den Link - dieser ist aber für meine Mini-App "way over the top" 😃

Einstweilen vielen Dank.
Gruß
Vorph

14.02.2020 - 15:58 Uhr

Hallo,

Ich möchte ein Datagrid mit Daten befüllen (MVVM). In meinem ViewModel sieht das dann so aus:


<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding DummyStudents}">
                <DataGrid.Columns >
                    <DataGridTextColumn Header="Nachname" Binding="{Binding LastName}"></DataGridTextColumn>
                    <DataGridTextColumn Header="Vorname" Binding="{Binding PreName}"></DataGridTextColumn>
                    <DataGridTextColumn Header="Klasse" Binding="{Binding ClassLabel}"></DataGridTextColumn>
                    <DataGridTextColumn Header="Bildungsgang" Binding="{Binding EducationType}"></DataGridTextColumn>
                    <DataGridTextColumn Header="Mathematik" Binding="{Binding Subjects.GradeAverageValToStr}"></DataGridTextColumn>
                    
                </DataGrid.Columns>
            </DataGrid>

Soweit funktioniert mein Code - ich bekomme die in 'DummyStudents (eine ObservableCollection<Student>) hinterlegten Objekte angezeigt. Bis auf die 5. Column, denn hier müsste ich auf das Property einer verschachtelten Klasse zugreifen. (public class Student--> List<Subject>Subjects--> hier bit es 10 Einträge, jeder sollte einer extra Column zugeordnet werden.)

Ein Student-Objekt schaut bei mir so aus:



        public string PreName { get; set; }
       
        public string LastName { get; set; }        
              
        public string ClassLabel { get; set; }
     
        public string EducationType { get; set; }
        
        public List<Subject> Subjects { get; set; } = new List<Subject>();

Interessant wird es bei List<Subject> Subjects. Der Typ Subject ist wiederum eine Klasse mit ein paar Properties und wiederum einer List<Grades> Grades (Schulnoten also) usw. Diese sind keine ObservableCollections, sondern eben Lists.

Bisher habe ich es immer so gemacht, dass ich sämtliche für die GUI relevanten Datenfelder in Form gesonderter ObservableCollections oder Properties in meinem ViewModel implementiert hatte.

Meine Frage: gibt es da noch einen anderen Weg? Denn


Binding="{Binding Subjects.GradeAverageValToStr}"

funktioniert nicht - meine ItemsSource ist bereits eine ObservableCollection und 'Subjects' ist eine List<Subject>, die zum Typ 'Student' gehört.

Ich hoffe, ich konnte das Problem einigermaßen genau skizzieren: vereinfacht: was tun, bei verschachtelten Klassen (speziell Objekte, die wieder Listen anderer Objekte bereithalten)?

Gruß
Vorph

12.02.2020 - 16:35 Uhr

Vielen Dank, Palladin007 - genau das war das Problem (Facepalm)!
Kaum macht man's richtig - schon funktioniert's!

Gruß
Vorph

11.02.2020 - 22:06 Uhr

Guten Abend,

ich versuche mehrere Views an ein TabControl zu binden.
Ich möchte meine Views und die zugehörigen ViewModels unter TabControl.Resources implementieren:


<TabControl TabStripPlacement="Left" ItemsSource="{Binding ManagerViewModels}"  SelectedIndex="0">

            <TabControl.Resources>
                
                <DataTemplate x:Key="ShowOverview" DataType="{x:Type viewModels:OverviewViewModel}">
                    <views:OverviewView/>
                </DataTemplate>

                <DataTemplate x:Key="ShowSchoolClass" DataType="{x:Type viewModels:SchoolClassViewModel}">
                    <views:SchoolClassView/>
                </DataTemplate>

                <DataTemplate x:Key="ShowGradeCalculator" DataType="{x:Type viewModels:GradeCalculatorViewModel}">
                    <views:GradeCalculatorView/>
                </DataTemplate>
              
            </TabControl.Resources>

            <TabControl.ItemContainerStyle>
                <Style TargetType="{x:Type TabItem}">
                    <Setter Property="Header" Value="{Binding VMLabel}" />
                    
                </Style>
            </TabControl.ItemContainerStyle>
                        
        </TabControl>

Alle ViewModels sind gleich aufgebaut und enthalten derzeit lediglich ein string Property


public class OverviewViewModel
    {
        public string VMLabel { get; set; }

        /// <summary>
        /// Constructor of the OverviewViewModel-Class
        /// </summary>
        public OverviewViewModel()
        {
            VMLabel = "Übersicht";
        }
       
    }

Problem: Wenn ich meine App starte, zeigen die Header des TabControl erwartungsgemäß den im jeweiligen ViewModel für VMLabel hinterlegten string. Aber für die Views im Content-Bereich bekomme ich lediglich einen string wie z. B.:
"StudentManager:ViewModels.OverviewViewModel° --> s. angehängter Screenshot

Die Views sind verschiedene UserControls, die derzeit nur mit einem Grid gefüllt sind, dessen Hintergrundfarbe ich verändert habe. Beispiel:


<UserControl x:Class="StudentManager.Views.CertificatesView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            ...
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid Background="Purple">            
    </Grid>
</UserControl>

Wenn ich die einzelnen Tabs aktiviere, sollte ich dann nicht statt der Pfadangabe die jeweilige Hintergundfarbe des Grids des entsprechenden UserControls sehen?

Hier ist noch mein MainWindowViewModel


public class MainWindowViewModel : NotifyPropertyChangedBase
    {        
        public ObservableCollection<object> ManagerViewModels { get; set; } 

        public MainWindowViewModel()
        {
            ManagerViewModels = new ObservableCollection<object>
            {
                new OverviewViewModel(),
                new SchoolClassViewModel(),
                new GradeCalculatorViewModel(),
                new CertificateViewModel(),
                new SettingsViewModel()
            };
        }        
    }

Vielen Dank,
Gruß
Vorph

26.01.2020 - 22:36 Uhr

Danke für den Link, Mr.Sparkle - das Kapitel mit über's Debugging hat mir schon mal gleich geholfen =)

Wobei mir trotz deines umfangreichen Textes nicht klar ist, was du eigentlich genau erreichen willst.

War zu viel Info, wa? 😁
Mein UserControl verfügt über ein Rectangle und eine TextBox. Das Rectangle besitzt ein Binding auf ein Property des ViewModels, das meine View darüber unterrichtet, ob ein Kapite in einem Buchl schon kommentiert wurde (registeredChapter) oder nicht (unregisteredChapter). Einmal Farbton grün - einmal rot.

Jetzt, wo ich weiß wie es geht - eigentlich total simpel. Meine Lösung s. oben.

as Binding mit ElementName bezieht sich auf ein Steuerelement in der View, an dessen Eigenschaften du binden kannst. Ansonsten bezieht sich das Binding auf den DataContext des Elements.

Den Teil hatte ich schon verstanden. Was ich nicht verstehe: der DataContext des Elements 'combo' ist das PathControlViewModel. Daran ist es gebunden.
In meinem ersten Versuch das Rectangle einzufärben hatte ich einfach

<DataTrigger Binding="{Binding CurrentChapterItem.ChapterStatus}" Value="registeredChapter">

im Code. Ohne ElementName. Dabei ist CurrentChapterItem ja genau jenes Property, das sich im DataContext von 'combo' befindet.
Meine Frage war also mehr oder minder: Müssten nicht beide Bindings (s. meinen vorherigen Post) zum selben Ergebnis führen?
Ich weiß, ich hab' da einen Denkfehler, komme aber nicht drauf...

26.01.2020 - 15:59 Uhr

Was genau ist deine Frage?

Sorry, das ging im Gedränge wohl unter:
Ich hatte es mit deiner Hilfe geschafft den Text im TextBlock der ComboBox je nach "Zustand" eines enums farblich zu verändern. Das Gleiche wollte ich jetzt auf ein Rectangle anwenden, bin aber gestern abend/heute früh stundenlang gescheitert.

Obwohl ich mir sicher war, dass ich es an diesem Punkt verstanden hatte - ich konnte mir leider nicht erklären, wo mein Fehler lag. Fälschlicherweise habe ich mir zu früh Asche auf's Haupt gestreut, weil mein Fazit das war, dass ich es total falsch versuche.

Mittlerweile habe ich die Lösung gefunden --> mein Code musste nur um folgende Zeile ergänzt werden:

<DataTrigger Binding = "{Binding ElementName=combo, path=SelectedItem.ChapterStatus}" Value="registeredChapter" />

Hingegen hatte ich zuvor:

<DataTrigger Binding="{Binding CurrentChapterItem.ChapterStatus}" Value="registeredChapter">

CurrentChapterItem ist ein Property des ViewModels. Meine Combobox binded bereits per SelectedItem an dieses Property, so dass ich der irrigen (?) Annahme war, ich könnte mit dem Rectangle genauso verfahren. Klappt dann aber nicht. Stattdessen musste ich ausdrücklich die Combobox referenzieren (Binding ElementName=comb0).

Gut, ich hab's ja jetzt hinbekommen. Dennoch, konkrete Frage: worin liegt der Unterschied zwischen
"Binding CurrentChapterItem.ChapterStatus" und "Binding ElementName=combo, Path=SelectedItem"?

Ich meine, ich verstehe schon, dass sich ElementName auf die ComboBox direkt bezieht, was ich ich nicht verstehe ist die Tatsache, dass das SelectedItem-Property der ComboBox auf das CurrentChapterItem des ViewModels bindet. Wenn ich aber das Rectangle direkt an genau dasselbe Property binde, reagiert es nicht. Hat das damit zu tun, dass sich das Rectangle im DataTemplate der ComboBox befindet?.
Gruß
Vorph

26.01.2020 - 10:01 Uhr

Womöglich habe ich die Antworten auf meinen letzten Post schon gefunden - wiedermal ist es aber so, das die für mich wesentlichen Fragen offen bleiben:

Einmal von Microsoft selbst (https://docs.microsoft.com/de-de/dotnet/api/system.windows.controls.itemscontrol.itemtemplate?view=netframework-4.8 ): Dort heißt es unter Hinweise:

Beispielsweise sind die generierten Container für ListBoxListBoxItem-Steuerelementen. bei ComboBoxhandelt es sich um ComboBoxItem-Steuerelemente. Verwenden Sie die ItemsPanel-Eigenschaft, um das Layout der Elemente zu beeinflussen.

Also, wenn ich das richtig interpretiere, kann ich das Rectangle irgendwie unter der ItemsPanel-Eigenschaft der ComboBox referenzieren oder einbinden und dann einen Style zuweisen. (Wobei ich das jetzt eher so lese, dass es hier um das generelle Erscheinungsbild geht, nicht um ein spezifisches, je nach UseCase?)

Und auch dieser Blog () lässt mich eher erahnen wie eine Lösung aussehen könnte, denn konkret behilflich zu sein. Interessanter Weise wird dasselbe Beispiel aufgenommen, wie von Microsoft (ohne Verweis auf die Quelle - na, wenn das mal kein Ärger gibt 😉 ): https://www.lernmoment.de/csharp-programmieren/datatemplate-stelle-mhelos-details-deiner-objekte-in-wpf-dar/

Insgesamt eigentlich verständlich erklärt (und hier wird auch klar, dass ich das UserControl wirklich nicht brauche), aber an den entscheidenden Stellen hätte ich mich über ein "wie" gefreut. So heißt es etwa:

Neben der ItemTemplate Eigenschaft haben Controls für Auflistungen die Eigenschaft ItemTemplateSelector. Dieser kannst du einen eigenen DataTemplateSelector zuweisen. Nun kannst du nicht nur ein DataTemplate verwenden, sondern gleich mehrere. Dabei entscheidet der DataTemplateSelector basierend auf Werten in den anzuzeigenden Objekten, welches DataTemplate benutzt werden soll.

Damit sollen sich nun bestimmte Werte hervorheben lassen. Es folgt auch ein Code-Beispiel, leider ohne Angabe wo es zu verwenden ist (Code behind? ViewModel? Anderweitig?).

Vielleicht verrenne ich mich ja aber auch gerade - falls dem so ist, wäre ich um jeden Hinweis in die richtige Richtung dennoch dankbar. Vielleicht is mein vorletzter post ja gar nicht so falsch und benötigt lediglich kleinere Korrekturen?

Vielen Dank, schönen Sonntag
Vorph

26.01.2020 - 02:31 Uhr

Ah, da schau' an - kaum macht man's richtig, schon funktioniert's!


<ComboBox>
 <ComboBox.Style>
                <Style TargetType="ComboBox">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding CurrentChapterItem.ChapterStatus}" Value="registeredChapter">
                            <Setter Property="Foreground" Value="Red"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ComboBox.Style>
        </ComboBox>

Sehr schön 👅

Was mich an XAML generell frustet ist, dass - kaum glaubt man, die "Regeln" verstanden zu haben - sich gerade gelerntes selten direkt auf "next-level"-Niveau anwenden lässt. Und die Sache hier ist ein schönes Beispiel.

Ich habe in der ComboBox jetzt einen TextBlock und ein Rectangle. Sieht dann so aus:


<ComboBox x:Name="combo" Margin="10" MinWidth="80" VerticalAlignment="Center"                   
                    ItemsSource="{Binding Source={StaticResource ItemListSortedView}}" 
                    SelectedItem="{Binding CurrentChapterItem}">     
            ...

            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Rectangle x:Name="rect" Width="10" Height="10" Margin="0,0,10,0"/>
                        <TextBlock Text="{Binding ChapterID}"/>
                    </StackPanel>
                </DataTemplate>            
            </ComboBox.ItemTemplate>
            <ComboBox.Style>
                <Style TargetType="ComboBox">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding CurrentChapterItem.ChapterStatus}" Value="registeredChapter">
<!-- spätestens hier beschwert sich der Compiler --> rect ist als Name nicht bekannt -->
                            <Setter TargetName="rect" Property="Foreground" Value="Red"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ComboBox.Style>
        </ComboBox>

Meine Idee war, statt dem Text das Rectangle farblich hervorzuheben. Leider funktioniert es so scheinbar nicht. Ich habe es außerdem mit einem Style in den UserControl.Ressources versucht - das hat aber genauso wenig funktioniert. Ich verstehe nicht, warum's mit Text funktioniert, mit dem Rectangle aber nicht X(

Pfft - halb drei; viel zu spät, besser ab ins Bett, das wird heut nix mehr 🙄

25.01.2020 - 21:11 Uhr

Verwende dafür einfach einen Trigger in der View.

Also einen DataTrigger? Und den binde ich dann einfach zu meinem Property? (In meinem Fall würde das Property so aussehen:


private PathItemState _itemState;

public PathItemState ItemState
{
get {return _itemState;}
set
{
 _itemState = value;
OnPropertyChanged(ref _itemState, value);
}

)

PathItemState ist ein enum; sieht folgendermaßen aus:


public enum PathItemState
{
chapterEmpty = 0,
chapterCompleted = 1,

}

Also muss mein Setter im DataTrigger auf dieses Property zeigen, und für Value wähle ich dann den jeweiligen "Zustand" aus, bei dem der Trigger anspringen und die Farbe in der TextBox des UserControls verändern soll.

Was willst du mit diesem Code erreichen:
...
Soll es evtl. so aussehen:

 public Color TheColor { get { return Color.Red; } }  

🤔 ja, so hätte es aussehen sollen. Offenbar ist der Kaffee nicht mehr das, was er mal war....

Und wozu brauchst du das UserControl eigentlich? Das braucht man wirklich nur, um Steuerelemente zu erstellen, die man in anderen Projekten wiederverwenden möchte, unabhängig von irgendwelchen ViewModels. Ansonsten reicht ein Template völlig aus.

Ich bin mir nicht 100% sicher, ob ich es mit einem Template auch so hinbekomme: ich möchte Kapitel in einem Buch kommentieren können; die Kommentare werden auf dem MainWindow dynamisch erstellt (Mist - jetzt wo ich das tippe, denke ich, "das ist womöglich genau DAS, wofür man sonst Templates verwendet..."). "Brauchen" ist vielleicht zuviel gesagt: ich bin lediglich Hobby-Coder und hatte neulich einen Blog über UserControls durchwühlt - hier hat es sich für mich einfach angeboten ein bisschen zu üben und zu experimentieren...

Noch einen Hinweis über die SAche mit dem Text="{Binding}" im Gegensatz zu Text="{Binding SelectedItem}"? Ich kann immernoch nicht ganz verorten, was ein {Binding} ohne Target-Property eigentlich bedeutet - woher weß Text (bzw. Binding) an welches Property er sich in diesem Fall binden soll?

Gruß
Vorph

25.01.2020 - 18:04 Uhr

Hallo,

bei meinem Lernprojekt habe ich eine einfache Sache vor, stelle aber gerade fest, dass es scheinbar doch nicht so leicht ist.

Ich habe ein userControl, in dem sich ein TextBlock befindet. Dieser soll später im Projekt den enthaltenen Text je nach Zustand eines Propertys in verschiedenen Farben anzeigen.
Ich habe:


<UserControl>
 <StackPanel Orientation="Horizontal">
        <ComboBox x:Name="combo" Margin="10" MinWidth="60" VerticalAlignment="Center" ItemsSource="{Binding Source={StaticResource ItemListSortedView}}" SelectedIndex="{Binding TheIndex, Mode=TwoWay}" SelectedItem="{Binding TheValue, Mode=TwoWay}">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="SelectionChanged">
                    <i:InvokeCommandAction Command="{Binding SelectionChangedCommand}"/>
                </i:EventTrigger>
                <i:EventTrigger EventName="DropDownOpened">
                    <i:InvokeCommandAction Command="{Binding SetOldValueCommand}" CommandParameter="{Binding ElementName=combo, Path=SelectedItem}"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
         
<!-- Hier der relevante Code-Teil -->
   <ComboBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding}" Foreground="{Binding Path=TheColor}"/>
                </DataTemplate>
            </ComboBox.ItemTemplate>
<!-- bis hier -->
        </ComboBox>
        <TextBox Margin="10" MinWidth="120" Text="{Binding TheText, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
    </StackPanel>
</UserControl>

Das Property "TheColor" ist im ViewModel meines UserControls hinterlegt und hat folgenden Code:

 public Color TheColor
        {
            get { return _color; }
            set
            {
                _color = Color.Red;
            }
        }

Bislang wird der Code jedoch gar nicht aufgerufen, was mich gleich zu den wichtigsten Fragen bringt.

  1. Zuerst hatte ich für den TextBlock
<TextBlock Text="{Binding SelectedItem}" ...</TextBlock>

was aber nur zur folge hatte, dass das selektierte Item so oft angezeigt wurde, wie Elemente in der Collection waren. Mehr oder minder durch Zufall habe ich herausgefunden, das einfach {Binding} zum gewünschten Ergebnis führt. Ich verstehe das nicht - gibt es dazu eine einfache, anschauliche Erklärung?

  1. Zu Testzwecken möchte ich die Textfarbe über ein Property (TheColor) setzen; später brauche ich jedoch ein DataTemplate (?), bzw. DataTriggers, weil sich die Farbgebung für den Text nach einem enum im Property richten soll (das Property TheColor wäre dann nicht vom Typ "Color", sondern ein enum). Wie müsste ich den Code dafür anlegen?

Gruß
Vorph

25.01.2020 - 17:51 Uhr

Hallo,

ich lerne zwar selbst noch, aber ich glaube ein paar Tipps kann ich dir geben:

1.

Ich habe folgende Ordner angelgt.

View
Ansicht1 (UserControl)
Ansicht2 (UserControl)
ViewModel
ViewModel1
Model
Model1

Da wird's schon mal sehr unübersichtlich. Als Ordner im Projekt-Explorer würde ich dir Models, Views und ViewModels vorschlagen. Außerdem möchte ich dir dringend raten auch für "Testprojekte" sinnvolle Namen für Ordner, Dateien und Variablen zu wählen, insbesondere, wenn du Hilfe brauchst. Denn für einen Außenstehenden ist nicht klar, welche Funktion ViewModel und ViewModel1 haben sollen. Worin unterscheiden sie sich?

  1. Im MainWindow solltest du gar keinen Verweis auf dein ViewModel setzen - das passiert besser im XAML-code. In deinem Fall:
  <Window.DataContext>
        <local:ViewModel1/>
    </Window.DataContext>

Leider bin ich mir nicht sicher, was du mit "anderer View je nach Auswahl" meinst - eine andere Ansicht (page), ein dynamisch erzeugtes UserControl (das entnehme ich jetzt mal aus Ansicht1 + 2)?

Dein UserControl bekommt ein eigenes ViewModel, z.B. Ansicht1ViewModel. In deinem Mainwindow, dort wo das UserControl erzeugt werden soll, musst du einen Verweis auf dein UserControl anlegen:

<StackPanel>
            
            <ItemsControl ItemsSource="{Binding Ansicht1ViewModel}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <local:Ansicht1/>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>

        </StackPanel>

Vielleicht hilft das ja schon.
Gruß
Vorph

20.01.2020 - 16:03 Uhr

Hallo,

vielen Dank für die Infos - ich hab's hinbekommen! Hier der Code, wie er hätte sein müssen, damit es funktioniert:

In der View des UserControls


<UserControl x:Class="UserControlTest.PathControl"
             <!-- ...-->
             
             <!--diesen Namespace einfügen-->
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
             <!-- ...-->            
    
    <StackPanel Orientation="Horizontal">
        <ComboBox x:Name="combo" Margin="10" MinWidth="60" VerticalAlignment="Center" ItemsSource="{Binding AvailableNumbers}" SelectedIndex="{Binding TheIndex}" SelectedItem="{Binding TheValue, Mode=TwoWay}">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="SelectionChanged">
                    <i:InvokeCommandAction Command="{Binding SelectionChangedCommand}"/>
                </i:EventTrigger>                
            </i:Interaction.Triggers>
        </ComboBox>
        <TextBox Margin="10" MinWidth="120" Text="{Binding TheText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
    </StackPanel>
    
    <!-- -->
</UserControl>


Im ViewModel des UserControls wird dann einfach das SelectionChangedCommand angemeldet - fertig. Danke für das Zauberwort, KroaX - tatsächlich fehlt mir manchmal das Vokabular 🤔

Aber:

Wenn der Benutzer einen Wert in der ComboBox auswählt, wird durch das Binding die Eigenschaft TheValue im ViewModel geändert. Wenn du dort im Setter auf die Änderung reagierst, brauchst du kein Event, kein Command und kein Package.

Ich frage mich ja, warum ich da nicht gleich draufgekommen bin! Ich überlege, ob das nicht sogar sauberer ist; denn tatsächlich brauche ich ja nur den geänderten Wert! Die View selbst interssiert sich in meinem Fall gar nicht mehr dafür, was mit dem Wert passiert; so vom Gefühl her würde ich sagen, dass man vlt. auf das SelectionChanged-Event nur dann mit einem EventToCommand reagieren sollte, wenn dadurch eine Relevanz für die View gegeben ist (veränderte Darstellungsoptionen, etc). Da ich hier aber aus dem Bauch raus argumentiere, kann und will ich dafür nicht die Hand ins Feuer legen. Es würde mich aber schon interessieren, wie sich das verhält, oder ob es schlicht Wurscht ist.

Nochmals Danke, macht's gut,
Vorph

19.01.2020 - 22:44 Uhr

Hallo Papst,

vielen Dank erstmal. Der von dir verlinkte Post auf StackOverflow war mein Ausgangspunkt; zwar motzt der Compiler nicht, wenn ich d: schreibe, aber du hast natürlich Recht.

Ich habe außerdem herausgefunden, dass man sich die Library für Interativity über NuGet-Konsole holen kann. Jedoch - ich bin derzeit etwas ratlos WAS ich genau in die Anweisung für die Event.Trigger schreiben muss



<i:Interaction.Triggers>
    <i:EventTrigger>
        <!-- ??? -->
    </i:EventTrigger>
</i:Interaction.Triggers>


[Im code, den ich zuvor gepostet habe, wird noch eine Klasse InvokeCommandAction aufgerufen - die aber bei mir gar nicht existiert 8o kann also auch nicht funktionieren.] --> EDIT: Blödsinn gelabert - bitte ignorieren!

Wie gesagt, Fehlermeldungen bekomme ich keine und das Ganze wird auch kompiliert.

Ich bin derzeit leider nicht an einem PC mit meinem Projekt; muss also leider bis morgen warten. Frage mich aber, wass ich nun anstelle der Fragezeichen ansprechen soll? Setter?

Gruß
Vorph

19.01.2020 - 19:13 Uhr

Hallo,

ich glaube, es ist nur ein kleiner Denkfehler, der mich gerade vom Weiterkommen abhält, aber ich bekommes es alleine nicht hin.

Einer ObservableCollection vom Typ int soll immer das in einer ComboBox ausgewählte Element hinzugefügt werden.

Das Problem: die ComboBox befindet sich auf einem UserControl, das dynamisch hinzugefügt wird. Ich müsste also, nach allem was ich zu wissen glaube, im ViewModel des UserControls auf den SelectionChange reagieren. Da ich gerade versuche MVVM zu lernen, soll der CodeBehind unangetastet bleiben.

Die einzige Methode, die ich mir nun vorstellen kann, ist im XAML-Code auf das "SelectionChanged"-Event zu reagieren, mit Hilfe eines Event-Triggers. Sieht bei mir so aus:

<UserControl x:Class="UserControlTest.PathControl"
             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"            <!-- stimmt das hier?-->

             xmlns:local="clr-namespace:UserControlTest"            
             mc:Ignorable="d" 
             d:DesignHeight="60" d:DesignWidth="800">
   
    
    <StackPanel Orientation="Horizontal">

        <ComboBox x:Name="combo" Margin="10" MinWidth="60" VerticalAlignment="Center" ItemsSource="{Binding AvailableNumbers}" SelectedIndex="{Binding TheIndex}" SelectedItem="{Binding TheValue, Mode=TwoWay}">
            <d:Interaction.Triggers EventName="SelectionChanged">
                <d:InvokeCommandAction Command="{Binding SelectionChangedCommand}" CommandParameter="SelectedItem" />
            </d:Interaction.Triggers>
        </ComboBox>

        <TextBox Margin="10" MinWidth="120" Text="{Binding TheText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
    </StackPanel>
</UserControl>

Das Command (SelectionChangedCommand) stelle ich im ViewModel des UserControls bereit. Wenn ich ein Item aus der ComboBox auswähle, kann ich im Debugger jedoch sehen, das dieses Command nie ausgeführt wird.
Ich vermute derzeit zwei Fehler: die Deklaration des Namespace für Interactions (s. Kommentar im XAML-Code), bzw. ein fehlender Schritt in der UserControl ViewModel-Klasse?

viele Grüße
Vorph

08.01.2020 - 20:25 Uhr

Cool - ich hab's so umgesetzt und hinbekommen 😃
Vielen Dank für deine Erklärungen, MarsStein! Besonders der letzte Post hat nochmal einiges geklärt!
Gruß
Vorph

05.01.2020 - 19:59 Uhr

Vielen Dank, MarsStein - So langsam ergibt das alles Sinn!

Ich habe deinen Code bisher nur gelesen, noch nicht getestet! Ich sehe wohin die Reise geht - wenn es erlaubt ist, noch ein paar Fragen dazu:

Zu Punkt 1: den Datacontext aus dem UserControl nehmen
Die UserControl wird ja wie ein Child behandelt, mit dem MainWindow als Parent, richtig? In diesem Fall, wenn ich den DAtaContext also an der Stelle rausnehme, verwendet das UC den DataContext des MainWindows? Andernfalls würde das Binding ja nicht funktionieren, oder?

Zu Punkt 3: Auftrennen der ViewModels
Kopfzerbrechen bereitet mir die Methode UCCreationExecute. Hier wird ja ein ViewModel (PathViewModel) initiiert. Kennt damit mein ItemsViewModel nicht automatisch mein PathViewModel? Ist das dann MVVM-konform? Oder ist das der Punkt, von dem du meintest

Wenn Du das einzeln steuern willst, gehört es aber ins PathViewModel und nicht ins MainViewModel (bzw. ItemsViewModel).

Falls ja, ist das exakt die Stelle, die ich nicht wirklich verstehe! Das Command wird ja vom MainViewModel (ItemsViewModel) ausgeführt, aber muss doch an der Stelle eine neue Instanz des PathViewModels erstellen, oder? Theoretisch müssten wir aber doch genau an der Stelle ItemsViewModel verlassen und es müsste in PathViewModel weitergehen - oder habe ich dich da falsch verstanden?

viele Grüße
Vorph

04.01.2020 - 22:49 Uhr

Danke für die Antwort. Allerdings verstehe ich einige Punkte nicht genau und möchte daher nochmal nachfragen:

Du erstellst mehrere ItemsViewModel-Instanzen.
Eines davon ist ans MainWindow gebunden, die anderen jeweils an ein PathControl.

Stimmt. Und ich weiß, dass im Ernstfall jede View ihr eigenes ViewModel haben sollte. Trotzdem verstehe ich nicht, warum es so nicht funktionieren sollte. Ich habe aber darüber nachgedacht und glaube folgendes Problem ausgemacht zu haben: ich bin bisher der Ansicht, wenn ich im XAML einer view eine local:ItemsViewModel-Variable deklariere, dass sie dann jede View zur genau gleichen Instanz referenziert. Das ist offenbar falsch?

Wenn Du jetzt Deinen Button zum Wechseln des Index klickst, wirkt sich das nur auf den Index des UserControls aus, das ans MainWindow gebunden ist -> deshalb kannst Du in den UsderControls keine Veränderung sehen.

Kapier ich jetzt nicht: welches UserControl habe ich denn ans MainWindow gebunden?

Die Vorgehensweise ist schon sehr obskur.

Das glaube ich dir sehr gerne - genau das passiert beim Lernen: man macht dumme Fehler. Aber die muss man halt erst mal verstehen. Genau deshalb frage ich ja.

Beim Erstellen eines neuen UserControls solltest Du dann lediglich das ItemsViewModel erzeugen (nicht das Control selbst - das widerspricht MVVM, weil Dein ViewModel Teile der View kennt!)

Das wusste ich bereits, aber um ehrlich zu sein, ist mir ein Rätsel, wie das geht. Wie genau erzeuge ich denn das ItemsViewModel, wenn nicht im Code behind? Dazu muss ich doch ins XAML, oder? Aber wie schaffe ich es dann, dass bei jedem Button-Click ein neues kommt? Und wo packe ich das in die von dir erwähnte Observable-Collection? Dazu brauch ich doch ein Objekt der View, oder?

Es würde mich freuen, wenn du mir dazu mal ein Code Beispiel zeigen könntest.

Gruß, Vorph

03.01.2020 - 21:25 Uhr

Hallo Forum,

vor meiner Frage zunächst euch allen ein gutes neues Jahr 😃

Ich experimentiere derzeit mit zur Runtime erstellten UserControls und habe folgendes Problem:

Mein UserControl besteht aus einem StackPanel in dem sich eine ComboBox und ein Textfeld befinden.

Die Combobox wird per Binding mit Zahlen 1-5 befüllt. Der Index der Combobox ist an ein Property des ViewModels gebunden (TheIndex). Bei jeder Erstellung des UserControls wird dieses Property per Zufallszahl generiert. Obwohl also die ItemsSource immer dafür sorgt, dass die Zahlen 1-5 enthalten sind, soll jedes neu erstellte UserControl zunächst eine andere Zahl anzeigen.

(Damit nur zulässige Indices entstehen, befülle ich die ComboBox mit 5 Zahlen und sorge dafür, dass die Zufallszahl stets zwischen 0 und 5 liegt.)

Dieses UserControl wird per Button-Click auf meinem MainWindow in ein StackPanel geladen.
Bei jedem Button-Click kommt nun auch ein neues UserControl hinzu, jedoch ist der Index immer derselbe (nämlich 0).

Code - UserControl- View


<UserControl x:Class="UserControlTest.PathControl"
             ...
    <UserControl.DataContext>
        <local:ItemsViewModel/>
    </UserControl.DataContext>
    
    <StackPanel Orientation="Horizontal">
        <ComboBox Margin="10" MinWidth="60" VerticalAlignment="Center" ItemsSource="{Binding  AvailableNumbers}" SelectedIndex="{Binding TheIndex}"/>
        <TextBox Margin="10" MinWidth="120"/>
    </StackPanel>
</UserControl>

Code - MainWindow- View


<Window x:Class="UserControlTest.MainWindow"
        ...
    <Window.DataContext>
        <local:ItemsViewModel/>
    </Window.DataContext>
    
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <StackPanel x:Name="HostPanel">
            <ItemsControl ItemsSource="{Binding PathControls}">
                
            </ItemsControl>
            
        </StackPanel>
        <StackPanel Grid.Column="1">
        <Button Command="{Binding UCCreationCommand}" Content="Add User Control" Margin="10"/>
        <Button Command="{Binding UCDeletionCommand}" Content="Delete User Control" Margin="10"/>
        <Button Command="{Binding ChangeIndexCommand}" Content="Change Index" Margin="10"/>
        </StackPanel>
    </Grid>
</Window>

Und hier der Code behind der ItemsViewModel-klasse


 public ObservableCollection<int> AvailableNumbers { get; set; } = new ObservableCollection<int>();
        public ObservableCollection<PathControl> PathControls { get; set; } = new ObservableCollection<PathControl>();

        private int _theIndex;

        public int TheIndex
        {
            get { return _theIndex; }
            set 
            { 
                _theIndex = value;
                OnPropertyChanged(ref _theIndex, value);       
            }
        }

        public ItemsViewModel()
        {
            AvailableNumbers.Add(1);
            AvailableNumbers.Add(2);
            AvailableNumbers.Add(3);
            AvailableNumbers.Add(4);
            AvailableNumbers.Add(5);

            UCCreationCommand = new CommandDelegateBase(UCCreationExecute, UCCreationCanExecute);
            UCDeletionCommand = new CommandDelegateBase(UCDeletionExecute, UCDeletionCanExecute);
            ChangeIndexCommand = new CommandDelegateBase(IndexExecute, IndexCanExecute);
            
        }

        public ICommand UCCreationCommand { get; set; }
        public ICommand UCDeletionCommand { get; set; }
        public ICommand ChangeIndexCommand { get; set; }
        //public event EventHandler<string> OnUCCreationEvent;


        private bool UCCreationCanExecute(object paramerter)
        {
            if (PathControls.Count < 8)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        private void UCCreationExecute(object parameter)
        {           
            PathControl p = new PathControl();          

            PathControls.Add(p);
        }

        private bool UCDeletionCanExecute(object paramerter)
        {
            if (PathControls.Count != 0)
            {
                return true;
            }
            else
            {
                return false;
            }

        }

        private void UCDeletionExecute(object parameter)
        {
            PathControls.RemoveAt(PathControls.Count -1);
        }

         private bool IndexCanExecute(object paramerter)
        {
            return true;
        }

        private void IndexExecute(object parameter)
        {
            Random rnd = new Random();

            int x = rnd.Next(0, AvailableNumbers.Count);

            TheIndex = x;
        }
     
    }

Es ist im Übrigen egal, wann ich TheIndex verändere (d.h. vor oder nach der Erstellung des UserControls) - da passiert nichts, d.h. der Index bleibt 0. Einzig, wenn ich im Konstruktor des ItemsViewModels einen anderen Wert für den Index setze, z. B. 3, dann bekomme ich tatsächlich die 4 angezeigt. Aber auch dann: ändere ich den Index zu einem späteren Zeitpunkt, bleibt alles bei "3".

Noch eine letzte Anmerkung: ich übe noch MVVM und bin mir sicher, dass es nicht das richtige Vorgehen ist, wenn das UserControl aus der ItemsViewModel-Klasse heraus instanziiert wird, sondern ich eine UserControlViewModel-Klasse bräuchte. Ich habe zum jetzigen Zeitpunkt leider keine Ahnung, wie man das bewerkstelligt. Also bitte erst einen Schritt nach dem anderen! Danke.

viele Grüße
Vorph

11.08.2019 - 19:56 Uhr

@Mr.Sparkle: Danke, für die Links zu den Artikeln - das wird etwas dauern, bis ich das alles "verdaut" habe. Mir würde es gerade sehr helfen, wenn ich wüsste wo genau mein Code suboptimal ist. Hättest du Tipps für einen Hobbyisten, wie ich mich der Thematik sinnvoll nähern kann (also womit sich zuerst beschäftigen, um dann darauf sinnvoll aufzubauen?)?

Die Idee der UserControl ist so auch nicht von mir - ich hatte dazu einen englischsprachigen Block gelesen, in dem es um UserControl, bzw. CustomControls ging. Dort was zu lesen, dass UserControls immer dann zum Einsatz kommen, wenn man ein Element sucht, bei dem einfache Objekte zusammengefasst werden und diese an verschiedenen Stellen gebraucht werden. Stimmt das so nicht?

Genau dafür gibt es doch DataBinding.

OK, sinnhaft besprochen, ich binde die Textbox an ein Model? Ich glaube, dann hätte ich zumindest so eine vage Ahnung, wo ich ansetzen müsste.

Einstweilen vielen Dank,
Gruß
Vorph

10.08.2019 - 21:43 Uhr

OK, nun noch eine Anschlussfrage (bin mir nicht sicher, ob das in einen neuen Thread sollte, aber es passt ja zu meiner Ausgangsfrage):

Ich habe das jetzt auch mit anderen Controls probiert und habe dazu eine Frage zu TextBoxen. Wenn ich diese dynamisch generiere, kann ich de dann im Code ansprechen? Über eine Liste, mit Indizes?

Ein super simples Beispiel:


<Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <StackPanel>
            <uc:UserControl_A x:Name="UC1" Grid.Column="2" Grid.Row="2"></uc:UserControl_A>
        </StackPanel>

    </Grid>

Hier mein User-Control:


<ItemsControl ItemsSource="{Binding Directives}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel>
                        <TextBox Margin="15,0">
                            <TextBox.Style>
                                <Style TargetType="TextBox">
                                    <Setter Property="Foreground" Value="Black" />
                                    <Setter Property="Text" Value="{Binding DirectiveMessage}"/>
                                    <Style.Triggers>
                                        <DataTrigger Binding="{Binding DirectiveState}" Value="hasMessage">
                                            <Setter Property="Foreground" Value="Red" />
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </TextBox.Style>
                        </TextBox>                       
                    </StackPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>

Die Datenobjekt-Klasse:


public class Directive
    {
        public string DirectiveMessage { get; set; }
        public DirectiveState DirectiveState {get;set;}
    }

    public enum DirectiveState
    {
        hasMessage = 0,
        hasNoMessage = 1
    }

Daten-Objekt-View


 public class MyView
    {
        public ObservableCollection<Directive> Directives { get; set; } = new ObservableCollection<Directive>();

        public MyView()
        {
            CreateDirectives();
        }

        private void CreateDirectives()
        {
            Directive d0 = new Directive();
            Directive d1 = new Directive();
            Directive d2 = new Directive();

            d0.DirectiveMessage = "abc";
            d0.DirectiveState = DirectiveState.hasMessage;

            d1.DirectiveMessage = "";
            d1.DirectiveState = DirectiveState.hasNoMessage;

            d2.DirectiveMessage = "def";
            d2.DirectiveState = DirectiveState.hasMessage;

            Directives.Add(d0);
            Directives.Add(d1);
            Directives.Add(d2);
        }

Uuuuuund MainWuindow.xaml.cs

MyView ViewTest { get; set; } = new MyView();

        public MainWindow()
        {
            InitializeComponent();

            DataContext = ViewTest;
        }

Das klappt soweit prima - aufgrund der DirectiveState-Property wird Text formatiert und über die ItemsControl des UserControl werden je nachdem wie viele Directives sich in der ObservableCollection "Directives" befinden, eine entsprechende Anzahl von TExtBoxen generiert. Wie würde man das jetzt angehen, wenn man z. B. in die 2. Textbox Text eingeben möchte und diesen dem DAtenobjekt wieder zuführen möchte; wie kann ich auf den Text dieser Textbox zugreifen? Nach welchen Begriffen müsste ich suchen, welche Stolperfallen ggf. umgehen?

Vielen Dank,
Gruß
Vorph

10.08.2019 - 21:28 Uhr

Ups - sorry. Ich dachte ich hätte es bei wpf und silverlight eingestellt.

10.08.2019 - 09:11 Uhr

Ah - ich hab's geahnt; der Wald und die Bäume und so 😁

ItemTemplate - kaum macht man's richtig, schon funktioniert's! Danke auch für den Link!
Vielen Dank,
Gruß
Vorph

09.08.2019 - 12:01 Uhr

Hallo,

in einem wpf-Project möchte ich eine vorher definierbare Anzahl an TextBlocks einem StackPanel hinzufügen.

Für Testzwecke habe ich drei Objekte erstellt [Typ <Paths> s. unten] deren Property PathMessage in jeweils drei TextBlocks angezeigt werden soll. Es wird aber immer nur der erste TextBlock mit Text angezeigt.

Mein Code:


 public class Path
 {
     public string PathMessage { get; set; }
 }

Die Klasse Path-Model:


public class PathModel
    {
        public ObservableCollection<Path> Paths { get; set; }

        public PathModel()
        {
            Paths = new ObservableCollection<Path>();            
        }
            
    }

Programmeinstieg:


 public partial class MainWindow : Window
    {
        PathModel pModel { get; set; }

        public MainWindow()
        {
            InitializeComponent();

            PathModel PathModel = new PathModel();

            pModel = PathModel;

            MyStack.DataContext = pModel.Paths;

            FillMyStack();

        }        

        private void FillMyStack()
        {
            Path p1 = new Path();
            Path p2 = new Path();
            Path p3 = new Path();

            p1.PathMessage = "Ich bin Pfad Nummer eins";

            p2.PathMessage = "Ich bin der zweite Pfad";

            p3.PathMessage = "Pfad Nummer drei: das bin ich";

            pModel.Paths.Add(p1);
            pModel.Paths.Add(p2);
            pModel.Paths.Add(p3);
        }
    }

Und mein XAML-Code: Ich vermute hier liegt der Fehler - das Binding findet zwar statt, aber wie gesagt: es wird nur der erste Eintrag meiner ObservableCollection angezeigt:


        <StackPanel x:Name="MyStackl">
            <ItemsControl ItemsSource="{Binding Paths}">
                <ItemsControl.Template>
                    <ControlTemplate>
                        <TextBlock Text="{Binding PathMessage}"/>
                    </ControlTemplate>
                </ItemsControl.Template>
            </ItemsControl>            
        </StackPanel>

Vielen Dank schon mal im Voraus - wahrscheinlich sehe ich den Wald vor lauter Bäumen nicht...

Gruß
Vorph

12.07.2019 - 17:07 Uhr

Hallo,

ich versuche mich gerade daran einen runden Button für ein wpf-Projekt zu erstellen. Die Form gelingt und im Prinzip bin ich mit dem Ergebnis zufrieden. Leider habe ich ein Problem bei den Triggern für verschiedene Button-Events, z. B. isMouseover etc. Ich möchte, dass beim Klicken und Hovern der Mouse sich die Farbe des Buttons ändert - das gelingt mir jedoch nur mit redundantem Code:

Style TargetType="Button">
            <Setter Property="Content" Value="Black"/>
            <Setter Property="Template">
                <Setter.Value>                    
                    <ControlTemplate>                        
                        <Grid>                            
                            <Path Stretch="Uniform" UseLayoutRounding="True" Fill="Aqua">                                
                                <Path.Data>                                    
                                    <EllipseGeometry RadiusX="1" RadiusY="1"/>                                    
                                </Path.Data>                                
                            </Path>                            
                        </Grid>                        
                    </ControlTemplate>                    
                </Setter.Value>                
            </Setter>
            
            <Style.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter Property="Cursor" Value="Hand"/>
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate>
                                <Grid>
                                    <Path Stretch="Uniform" UseLayoutRounding="True" Fill="Green">
                                        <Path.Data>
                                            <EllipseGeometry RadiusX="1" RadiusY="1"/>
                                        </Path.Data>
                                    </Path>
                                </Grid>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Trigger>

                <Trigger Property="IsPressed" Value="True">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate>
                                <Grid>
                                    <Path Stretch="Uniform" UseLayoutRounding="True" Fill="Orange">
                                        <Path.Data>
                                            <EllipseGeometry RadiusX="1" RadiusY="1"/>
                                        </Path.Data>
                                    </Path>
                                </Grid>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Trigger>

            </Style.Triggers>
        </Style>

Besonders dieser Teil riecht verdächtig nach code-smell:

<ControlTemplate>
                                <Grid>
                                    <Path Stretch="Uniform" UseLayoutRounding="True" Fill="Green">
                                        <Path.Data>
                                            <EllipseGeometry RadiusX="1" RadiusY="1"/>
                                        </Path.Data>
                                    </Path>
                                </Grid>
                            </ControlTemplate>

Oder bleibt mit hier wirklich nichts anderes übrig, als die Form ständig neu zu erzeugen? Könnte mir BasedOn hier weiterhelfen?

Außerdem stehe ich gerade völlig auf dem Schlauch, warum man bei einem Button im Projekt den Content (z. B. "KLick mich") nicht sehen kann.

Für alle Tipps, die einem Anfänger weiterhelfen schon mal vielen Dank!

Gruß
Vorph

22.06.2019 - 20:17 Uhr

SQLite, EntityFrameworkCore

Hallo,

nach langer Zeit möchte ich zum EntityFramework zurückkehren.
Ich habe mich beim Einrichten meines Projekts an die Anleitung auf folgender Website gehalten, und folgende Schritte unternommen:

  1. Folgende NuGet-Packages installiert:
  • Microsoft.EntityFrameworkCore.SQLite
  • Microsoft.EntityFrameworkCore.Tools
  • Microsoft.EntityFrameworkCore.Design
  1. In meinem Projekt eine DBContex erstellt:
public class MainDBContext : DbContext
    {
        /// <summary>
        /// Here go all Models/Objects that are part of the Database.
        /// They are the tables of the Database.
        /// </summary>
        #region DBSets
        public DbSet<Student> Students { get; set; }
        #endregion

        public MainDBContext()
            : base() { }

        protected override void OnConfiguring(DbContextOptionsBuilder optionBuilder)
        {
            optionBuilder.UseSqlite("Data Source = MyDataBase.sqlite");
        }
    }
  1. In der NuGet-Konsole folgendes Command eingeben, mit dem Ziel Migrationen zu erstellen:
    PM: add-migration InitialCreate

Dabei bekomme ich folgende Fehlermeldeung:> Fehlermeldung:

Ein Aufrufziel hat einen Ausnahmefehler verursacht.

(Es ist der einzige Fehler, der angezeigt wird, fast hatte ich den Eindruck, dass sonst alles funktioniert...)

Ich habe dann weiter oben in der Konsolenausgabe gesucht und bin auf Folgendes gestoßen:> Fehlermeldung:

System.Reflection.TargetInvocationException: Ein Aufrufziel hat einen Ausnahmefehler verursacht. ---> System.TypeLoadException: Der Typ "Microsoft.EntityFrameworkCore.Internal.LazyRef`1" in der Assembly "Microsoft.EntityFrameworkCore, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60" konnte nicht geladen werden.

Leider lässt mich die Fehlermeldung ratlos zurück - welche Schritte sind erforderlich, um diesen Fehler zu beheben?

Gruß
Vorph

P.S.: Gibt es eine gute Anleitung für den (Wieder-)Einstieg in EntityFramework Core und SQLite? Ich habe bisher im Internet wahrlich viel Mist gefunden...

20.06.2019 - 23:20 Uhr

So, ich muss zwei Dinge kurz klarstellen:

  1. ich code rein hobbymäßig, wenn Beruf und Familie es gestatten (d.h. alle paar Wochen mal ein paar Stunden). Einiges habe ich schon verstanden, vieles noch nicht.

  2. Anfängerfehler passieren daher immer wieder. Und manche Fehler sind halt "dumm". So wie dieser.

Problem gelöst!

Mein xaml-code funktioniert - lediglich die Event Handler gaaanz obe im xaml-code blieben unbemerkt. GetFocus und LostFocus. Dort stand tatsächlich noch Code drin, der die Visibility der TextBlocks auf Collapsed setzt. Kein Wunder, dass es nicht funktioniert hat.

Es geht also einfach (ganz einfach sogar) mit den Triggern. Man bekommt meinen Code zum Laufen, wenn man die beiden Event Handler löscht. Ohne FindAncestor, ohne BooleanToVisibilityConverter. Das sind andere Wege, die mit Sicherheit zur selben Lösung führen.

Und nachdem ich mir nun Asche auf's Haupt gestreut habe: den xaml-Code mit den Triggern hatte ich in einer Kaffee-Pause auf ne Serviette gekritzelt - da bin ich doch ein bissl stolz, dass ich zumindest das scheinbar verstanden habe 😉

Trotzdem Danke für eure Hilfe - das Forum ist und bleibt Wissensfundus Nr. 1.
Gruß
Vorph

20.06.2019 - 11:07 Uhr

Hallo KroaX und vielen Dank für deine Antwort.

Leider ist der Link etwas "beyond my scope" 😉
Aber ich denke, ich weiß in etwa wie es gemeint ist. Daher drei zusätzliche Fragen:

  1. gerade weil

[...]Style heraus auf Elemente zugreifen kannst du höchsten indem du den Visual Tree nach oben läufst. verstehe ich jetzt immer noch nicht, warum es beim ersten mal klappt?

  1. So wie ich das verstehe, müsste ich z. B. in der Window-Resource ein Style-Template für den Typ TextBlock anlegen Ist das richtig?
    Im Beispiel kann man es nicht sehen, aber ich möchte ja nur bestimmte TextBlocks ansprechen - das heißt ich arbeite hier mit x:Key?

Das könntest du machen indem du mit FindAncestor nach dem erstmöglichen TabItem suchst und dich daran bindest.

Muss ich das dann auch im Template unterbringen? Die Frage ist dann wo? Und wie kann ich an FindAncestor binden, denn der würde dann ja erst gesucht.
Es gibt keine Möglichkeit hier mit den x:Name-Attributen zu arbeiten? Denn die sind ja für die Tabs bekannt...

Vielen Dank,
schöne Grüße
Vorph

20.06.2019 - 08:45 Uhr

Hallo,

Folgende Funktionalität möchte ich herstellen:

In einem TabControl habe ich im Header jedes TabItems zwei TextBlocks. Im ersten TextBlock befindet sich die Bezeichnung des TabItems, im zweiten TextBlock ein Symbol-Font.
Ich möchte, dass jeweils der Text im ersten TextBlock des ausgewählten TabItems lesbar ist, sobald das TabItem Focus besitzt, d.h. die Visibility des ersten TextBlocks von Collapsed auf Visible geändert wird.

In Code behind ist es zwar ganz einfach, ich möchte es aber - wenn möglich - im xaml-code lösen.

hier mein Code:

<TabItem x:Name="Item2" GotFocus="Item2_GotFocus" LostFocus="Item2_LostFocus">                    
                    <TabItem.Header>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock x:Name="Text2" FontSize="14" FontFamily="Quicksand" VerticalAlignment="Center">
                                <TextBlock.Style>
                                    <Style TargetType="TextBlock">
                                        <Setter Property="Visibility" Value="Collapsed"/>
                                        <Style.Triggers>
                                            <DataTrigger Binding="{Binding ElementName=Item2, Path=IsFocused}" Value="True">
                                                <Setter Property="Visibility" Value="Visible"/>
                                            </DataTrigger>
                                        </Style.Triggers>
                                    </Style>
                                </TextBlock.Style>
                            Große<LineBreak/>Leistungsnachweise                                
                            </TextBlock>
                            <TextBlock Margin="20,0,0,0" FontSize="32" FontFamily="APPFONT" VerticalAlignment="Center">B</TextBlock>
                        </StackPanel>
                    </TabItem.Header>
                </TabItem>

Leider funktioniert obiger Code nur einmal - danach wird die Bezeichnung nicht mehr angezeigt, d.h. die Visibility des TextBlocks Text2 bleibt ständig auf "Collapsed".

Die Funktionalität sollte aber ständig gegeben sein: erhält das jeweilige TabItem Fokus, dann soll der jeweilige TextBloxk1 im Header sichtbar sein, verliert das TabItem den Fokus, soll der jewilige TextBlock collapsed sein.

Wahrscheinlich benutze ich gar nicht den richtigen Trigger? Brauche ich einen EventTrigger, der auf den Item2_GotFocus-Event reagiert? Leider scheinen EventTrigger nur mit StoryBoards zu funktionieren, aber ich will ja gar nichts animieren?

Zur Verdeutlichung ein Bild im Anhang (der von mir verwendete Font ist selbsterstellt, funktioniert also nicht bei jedem...).

Gruß Vorph