UWP und EntityFramework Lab .NET Standard 2.0

In diesem Lab lernt man eine einfache ToDo App mit direkter Anbindung an einen SQL Server zu erstellen ohne REST Web API Service. Mit der kommenden Windows Version werden weite Teile von .NET Bestandteil der WIndows API. Auch z.B. SQLClient aus ADO.NET. Da diese API dem .NET Standard folgt sind die Bibliotheken direkt in .NET Core, .NET Framework und .NET native nutzbar, Man erhält so mit UWP eine optisch aufgehübschte Alternative zu WPF.

Es wird benötigt (Stand 29.08.2017)

WIndows 10 Creators Update Fall Build 16267 (FCU)

Windows SDK Preview Build

Visual Studio 2017.4 Preview

SQL Server Developer Edition mit lokalem sa User

Starten Sie Visual Studio Preview erstellen ein C# Projekt vom Typ “leere App (Universal Windows App) und nennen diese ToDoCore. Die minimale Version muss der Build 16257 (FCU) sein.

todo2

In den Referenzen des Projektes findet sich ein Verweis auf Microsoft.NETCore.UniversalWindowsPlattform. Wählen Sie  den Nuget Paket Manager aus dem Projekt Menu von Visual Studio und lesen sie Version ab. Diese muss mindestens V6 enthalten. Suchen Sie im Dursuchen Reiter des Nuget Dialoges nach Microsoft.EntityFrameworkCore.SqlServer und installieren die Version 2.0.0 (preview 1 final).

todo3

Nun hat das Projekt 2 Referenzen und ist damit Startklar für UWP und EntityFramework 2 Core. Diese Version weist durchaus Unterschiede zu EntityFramework 6 für .net auf.

todo4

Fügen Sie dem Projekt eine Klasse Todo.cs hinzu, die das Model im MVVM Context darstellt. Implementieren Sie die Schnittstelle INotifyPropertyChanged. Es könnte sein das die üblichen Tools von Visual Studio den Namespace nicht automatisch erkennen. Fügen Sie dann die Zeile

using System.Componentmodel per Hand ein.

   1:  public class ToDo : INotifyPropertyChanged
   2:   
   3:      {
   4:   
   5:          public int ID { get; set; }
   6:   
   7:          private string _task;
   8:   
   9:          public string Task
  10:   
  11:          {
  12:   
  13:              get { return _task; }
  14:   
  15:              set { _task = value;
  16:   
  17:                  RaisePropertyChanged("Task"); }
  18:   
  19:          }
  20:   
  21:          public bool Done { get; set; } = false;
  22:   
  23:          public event PropertyChangedEventHandler PropertyChanged;
  24:   
  25:          void RaisePropertyChanged(string propertyName)
  26:   
  27:          {
  28:   
  29:              PropertyChanged?.Invoke(this, 
new PropertyChangedEventArgs(propertyName));
  30:   
  31:          }
  32:   
  33:      }

 

Wenn Property ID enthält und vom Typ GUID oder Int ist wird später in der Datenbank per Konvention automatisch eine Identity Spalte als Primary Key erstellt.

Um das Model zu komplettieren wird für EntityFramework eine weitere Klasse benötigt, die ToDoContext.cs benannt wird. In dieser werden die Modelklassen zusammengeführt und die Konfiguration per Code vorgenommen. Ggf. per Using den EntityFrameworkCore Namensraum einbinden.

   1:   class ToDoContext:DbContext
   2:   
   3:      {
   4:   
   5:         public DbSet<ToDo> ToDos { get; set; }
   6:   
   7:        public ToDoContext()
   8:   
   9:          {
  10:   
  11:  //              Database.EnsureCreated();
  12:   
  13:          }
  14:   
  15:          protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  16:   
  17:          {
  18:   
  19:              optionsBuilder.UseSqlServer(@"Data Source = 
desktop-k0ag27t; Initial Catalog = tododb; User Id = sa; Password = xxxx; "
);
  20:   
  21:          }
  22:   
  23:   
  24:   
  25:      }

Das auskommentierte Kommando Ensurecreated erzeugt bei Bedarf die Datenbank samt laut DBContext definierten DbSet Tabellen automatisch, sobald jemand eine Instanz der Klasse erstellt. Dies ist für Entwicklerzwecke sehr praktisch. Die gewohnten Assistenten für Code First Model Building sind in meiner Version von Visual Studio aktuell nicht vorhanden.

Um dazu einen Funktionstest zu starten, fügen Sie in der vorhandenen Datei MainPage.xaml.cs ein Codezeile nach InitializeComponent für die Instanziierung der ToDoContext Klasse ein und kommentieren Zeile 11 wieder ein (EnsureCreated).  Falls nun der Compiler Fehler meldet, installieren sie die Preview 2 Version von einem Daily Build Server in das Projekt.

https://dotnet.myget.org/feed/dotnet-core/package/nuget/Microsoft.NETCore.UniversalWindowsPlatform/6.0.0-preview2-25628-01

Da die Pakete teilweise mehrmals täglich neu erstellt werden, kann es hier zu Problemen kommen. Kein produktiver Einsatz sinnvoll. Dies funktioniert generell nur mit der 2017.4 Version von Visual Studio!

Wenn hier das Programm kompiliert, gestartet wird und keine Exception wirft, sind wir sozusagen durch.

Im Server Explorer von Visual Studio kann dann die Datenverbindung zum lokalen SQL Server mit den oben SA Benutzernamen und gewählten Passwort hergestellt werden. Per Rechtsclick (Tabellendaten anzeigen) auf die Tabelle Todos, wird per Kommando in den Editiermodus gewechselt und es lassen sich Einträge in die Tabelle hinzufügen.

todo5

Als nächstes wird das User Interface im XAML Code der Datei Mainpage.xaml definiert, wie oben dargestellt. Leider funktioniert aktuell der visuelle Designer in Visual Studio nicht. Im MVVM Kontext wird das gemeinhin als View bezeichnet.

todo1

   1:  <Grid  >
   2:          <Grid.ColumnDefinitions>
   3:              <ColumnDefinition Width="1*"></ColumnDefinition>
   4:              <ColumnDefinition Width="1*"/>
   5:          </Grid.ColumnDefinitions>
   6:          <StackPanel Width="300" Grid.Column="0">
   7:              <ListView x:Name="listView1" SelectionMode="Single" 
   8:                        SelectionChanged="listView1_SelectionChanged"
   9:                        ItemsSource="XXXX">
  10:                  <ListView.ItemTemplate>
  11:                      <DataTemplate x:DataType="XXXX">
  12:                          <StackPanel Orientation="Horizontal">
  13:                              <CheckBox IsChecked="XXXX" Width="50"
  14:                                           ></CheckBox>
  15:                              <TextBlock Text="XXXX"></TextBlock>
  16:                          </StackPanel>
  17:                      </DataTemplate>
  18:                  </ListView.ItemTemplate>
  19:               </ListView>
  20:              <Button Content="Done" Click="XXXX"></Button>
  21:          </StackPanel>
  22:          <StackPanel Grid.Column="1">
  23:              <TextBlock Text="ToDo Items"></TextBlock>
  24:              <TextBox Text="XXXX" 
  25:                       ></TextBox>
  26:              <StackPanel Orientation="Horizontal">
  27:                  <Button Content="Neu" Click="XXXX" Margin="5"></Button>
  28:                  <Button Content="update" Click="XXXX" Margin="5"></Button>
  29:              </StackPanel>
  30:              </StackPanel>
  31:   </Grid>

 

Alle Attribute mit dem Wert XXXX dienen hier nur als Platzhalter und werden später bei der Datenbindung Bindung ausgetauscht.

Nach dem View wird das Viewmodel wiederum als Klasse mit dem Namen ToDoVM.cs dem Projekt hinzugefügt. Das Viemodel beinhaltet als Property eine Liste der ToDo Tasks, den aktuellen ToDo Task und diverse Funktionen zum hinzufügen oder speichern. Wie im XAML Umfeld üblich wird das Interface INotifypropertyChanged (aus System.ComponentModel) implementiert um Änderungen in VIewModel dem View per Event mitzuteilen. Darüber hinaus wird die generische Liste aus System.Collections.Objectmodel benötigt. Dies ist nur erste Schritt des Viewmodels um die Anzeige der Liste der ToDo Items und das hinzufügen eines neuen Eintrages in die Tabelle zu demonstrieren.

   1:  public class ToDoVM : INotifyPropertyChanged
   2:      {
   3:          public ObservableCollection<ToDo> 
ToDoList { get; set; }= new ObservableCollection<ToDo>();
   4:          private ToDo _item;
   5:          public ToDoVM()
   6:          {
   7:              Item = new ToDo();
   8:          }
   9:          public ToDo Item
  10:          {
  11:              get { return _item; }
  12:              set { _item = value;
  13:                  RaisePropertyChanged("Item");
  14:              }
  15:          }
  16:          public event PropertyChangedEventHandler PropertyChanged;
  17:          public void SaveItem()
  18:          {
  19:              var ef = new ToDoContext();
  20:              ef.ToDos.Add(_item);
  21:              ef.SaveChanges();
  22:              Load();
  23:          }
  24:          public void Load()
  25:          {
  26:              var ef = new ToDoContext();
  27:              foreach (var t in ef.ToDos)
  28:              {
  29:                      ToDoList.Add(t);
  30:              }
  31:          }
  32:          void RaisePropertyChanged(string propertyName)
  33:          {
  34:              PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(propertyName));
  35:          }
  36:      }

Ein paar Hinweise, warum der Code so aussieht wie er ist. Eine Observable Collection aktualisiert das UI nur wenn ein Eintrag hinzugefügt oder gelöscht wird. Deswegen muss das für jeden Eintrag einzeln geschehen (foreach Zeile 29). Wenn der Benutzer neue Daten eingeben hat und auf den Speichern Button clickt, wird dem Datacontext in Zeile 20 der neue Eintrag vom Typ ToDo aus dem Formular hinzugefügt. Um die Anzeige in der Liste zu aktualisieren wird diese einfach neu geladen. Dies wird später noch im Code geändert. Das ganze Stück Software ist nur ein Prototyp und sollte beim zweiten neuen Task eine Exception werfen. Nicht vergessen den eingefügten Code in das Codebehind des Views Mainpage.Xaml.cs  wieder entfernen.

Nun das ViewModel mit dem View verbunden. Das geschieht bei UWP compiled Bindings ganz einfach in dem man schlichtweg das Viewmodel zum Property der Page erklärt.  Hinweis, nur wenn man MyVM bei der Deklaration mit einer Instanz von ToDoVM belegt funktioniert das Binding. Macht man das im Konstruktor, bleibt die Anzeige im Listview später leer. Die Liste wird dann geladen.

   1:   public ToDoVM MyVM { get; set; } = new ToDoVM();
   2:   public MainPage()
   3:        {
   4:              this.InitializeComponent();
   5:              MyVM.Load();
   6:          }
   7:          private void listView1_SelectionChanged(object sender,
SelectionChangedEventArgs e)
   8:          {
   9:              MyVM.SelectedID((ToDo) listView1.SelectedItem);
  10:          }
  11:   

In einem späteren Schritt wird der ausgewählte Eintrag in der Liste dem Formular zugewiesen, so das der Benutzer den Eintrag auch editieren kann. Ergänzen Sie den Code wie oben beschrieben und schliessen damit die Arbeiten an diesem C# File ab.

Nun wird wieder im Viewmodel das Binding an die Propertys und Funktionen des Views definiert. Das XAML Listview Controll erhält die Daten aus dem Property MyVM. Im Viewmodel wiederum ist die Liste als Eigenschaft ToDoList hinterlegt. Generell gilt das in UWP Compiled Bindings der default Wert OneTime ist und daher immer abweichend definiert sein muss. Besonderheit ist der Typ im Datatemplate, der auf die ToDo Model Klasse verweist.  Der Namensraum local muss von  Visual Studio automatsich im Kopfbereich mit einem Verweis auf die App deklariert sein. Die X: Bind Syntax ist ähnlich der {Binding} Syntax.

   1:  ItemsSource="{x:Bind MyVM.ToDoList,Mode=TwoWay}">
   2:  <ListView.ItemTemplate>
   3:        <DataTemplate x:DataType="local:ToDo">
   4:               <StackPanel Orientation="Horizontal">
   5:                  <CheckBox IsChecked="{x:Bind  Done,Mode=TwoWay}" Width="50"
   6:                    ></CheckBox>
   7:                  <TextBlock Text="{x:Bind  Task,Mode=OneWay}"></TextBlock>

 

Das Formular für Neu und Edit wird analog mit Binding versehen. Dabei sticht natürlich ins Auge wie einfach Button Events gebunden werden. Ganz ohne ICommand aus WPF.

   1:   <TextBox Text="{x:Bind MyVM.Item.Task,Mode=TwoWay}" 
   2:    ></TextBox>
   3:    <StackPanel Orientation="Horizontal">
   4:        <Button Content="Neu" Click="{x:Bind MyVM.AddNew}" Margin="5"></Button>
   5:        <Button Content="update" Click="{x:Bind MyVM.SaveItem}" Margin="5"></Button>
   6:  </StackPanel>

Im letzten Schritt wird die Benutzerauswahl eines Tasks implementiert und das speichern der erledigten Einträge. Dazu wird im ViewModel ToDoVM.cs eine Update Funktion erstellt, die Änderungen per EntityFramework in die Datenbank zurück schreibt. Die gewählte C# Logik nimmt die Liste und sucht sich aus der EF Liste den Eintrag und ändert den Wert für die Done Eigenschaft. Das abschliessende SaveChanges schreibt die Werte in die Datenbank zurück.

 

   1:  public void SelectedID(ToDo item)
   2:          {
   3:              Item = item;
   4:         }
   5:  public void UpdateDone()
   6:          {
   7:              var ef = new ToDoContext();
   8:              foreach (var item in ef.ToDos)
   9:              {
  10:                  item.Done = ToDoList.Where(id => id.ID == item.ID).First().Done;
  11:              }
  12:              ef.SaveChanges();
  13:          }

Im XAML muss noch das Attribut im Listview Control ergänzt werden um auf die Benutzerauswahl zu reagieren. Dadurch wird die Methode SelectedID im Viewmodel angestossen und das Formular mit dem ToDo Item belegt, statt dem leeren.

   1:  SelectionChanged="listView1_SelectionChanged"

In der Tat gibt es einige UWP Funktionen die so noch nicht sauber genutzt werden. So finden sich Animationen beim entfernen oder hinzufügen eines ListView Items. Entsprechend wird der Code der Load Methode angepasst, das nur einzelne Einträge hinzugefügt werden. Ausserdem kann nun die SaveItem Methode anhand der ID des ToDo Items erkennen ob es sich um eine neues (ID=0) oder ein altes Item handelt. Entsprechend wird nur gespeichert oder per Add neu eingefügt.

   1:  public class ToDoVM : INotifyPropertyChanged
   2:      {
   3:          public ObservableCollection<ToDo> ToDoList { get; set; }=
new ObservableCollection<ToDo>();
   4:          private ToDo _item;
   5:          public ToDoVM()
   6:          {
   7:              Item = new ToDo();
   8:          }
   9:          public ToDo Item
  10:          {
  11:              get { return _item; }
  12:              set { _item = value;
  13:                  RaisePropertyChanged("Item");
  14:              }
  15:         }
  16:         public event PropertyChangedEventHandler PropertyChanged;
  17:          public void AddNew()
  18:          {
  19:              Item = new ToDo();
  20:          }
  21:          public void SaveItem()
  22:          {
  23:              var ef = new ToDoContext();
  24:              if (_item.ID>0)
  25:              {
  26:                  var item=ef.ToDos.Find(_item.ID);
  27:                  ef.Entry(item).CurrentValues.SetValues(Item);
  28:                  ToDoList.Where(
id => id.ID == _item.ID).First().Task = _item.Task;
  29:              }
  30:              else
  31:              {
  32:                  ef.ToDos.Add(_item);
  33:              }
  34:              ef.SaveChanges();
  35:              Load();
  36:          }
  37:          public void Load()
  38:         {
  39:              var ef = new ToDoContext();
  40:              foreach (var t in ef.ToDos)
  41:              {
  42:                  if (ToDoList.Where(x => x.ID == t.ID).Count() == 0)
  43:                  {
  44:                      ToDoList.Add(t);
  45:                 }
  46:              }
  47:          }
  48:          public void SelectedID(ToDo item)
  49:         {
  50:              Item = item;
  51:          }
  52:         public void UpdateDone()
  53:          {
  54:              var ef = new ToDoContext();
  55:              foreach (var item in ef.ToDos)
  56:              {
  57:                  item.Done = ToDoList.Where(id => id.ID == item.ID).First().Done;
  58:              }
  59:              ef.SaveChanges();
  60:          }
  61:          void RaisePropertyChanged(string propertyName)
  62:          {
  63:              PropertyChanged?.Invoke(this, 
new PropertyChangedEventArgs(propertyName));
  64:          }
  65:      }

Wenn Probleme oder Fragen auftreten bitte als Issue dort anlegen https://github.com/hannespreishuber/App4EF

Kommentare sind geschlossen