WPF Childwindow selber bauen

In Silverlight gibt es einen praktischen ChildWindow Klassentyp. Wer dies bei WPF sucht muss auf das WPF Toolkit ausweichen. Dieses ist auf dem Stand 2010. Ein anderes Projekt, das behauptet in seine Fussstapfen getreten zu sein, hat eine etwas eigenartige Lizenzpolitik.

Also ein guter Zeitpunkt für ein Konzept wie ein Childwindow 2016 aussehen könnte und man dabei den Pfad von MVVM einschlägt.

Dem Gedanken des MVVM Patterns folgend wird ein View und ein Viewmodel erwartet. Das Viewmodel hält eine Liste von Customers und hat die Methoden um die Liste zu laden und einzelne Entitäten zu löschen, anzulegen oder zu editieren. Andere würde dafür je eigene Viewmodels bauen.

In der weiteren theoretische Vorbetrachtung schließe ich aus, aus dem Viewmodel  die Sicht direkt zu beeinflussen. Also nur der View kann z.B. Fenster öffnen oder schließen. Damit muss man zwangsläufig im Codebhind der XAML Datei auf Button Click Events reagieren. Ich empfinde das als natürlicher und zumeist ist auch weniger Code nötig.

Aus UX Design Sicht soll das aktive Fenster mit einer Art modalen Dialog überblendet werden, wenn der Benutzer auf den Button Neu drückt.

image

Die Logik sollte dann mit zwei Zeilen Code erledigt sein. Eine Klasse ChildWindow erhält als Parameter ein UserControl.

   1:   private void button1_Click(object sender, RoutedEventArgs e)
   2:          {
   3:              var wnd = new childwnd();
   4:              wnd.show(new addCustomer());

Diese AddCustomer UserControl erlaubt dem Designer getrennt vom MainWindow den Edit Dialog zu gestalten.

image

Im Viewmodel dient eine Property NewCustomer als bindbare Quelle für die Textboxen. Da das Usercontrol später in den XAML Tree der aufrufenden Seite geladen wird, klappt das ohne den Datacontext zuzuweisen.

Mit nahezu identer Logik lässt sich ein ICommand Property aus dem Viewmodel an das Command eines Buttons binden in dem die Daten letztendlich gespeichert werden.

   1:  <Button Content="Speichern" 
   2:          Command="{Binding SaveCustomerCommand}"
   3:          Click="button_Click"/>
   4:  <TextBox Text="{Binding newCustomer.ContactName}" />

Nicht ganz selbstverständlich lässt sich auch das Click Event des Buttons gleichzeitig mit dem Command belegen. Das erscheint mir unter dem Aspekt der Trennung der Aufgaben auch sinnvoll zu sein. Allerdings muss die Umwelt über die Absicht unterrichtet werden und dazu bietet sich ein Event an, dem wiederum jemand anderer lauschen/subscriben kann.

   1:   public event EventHandler CloseChild;
   2:   
   3:   private void button_Click(object sender, RoutedEventArgs e)
   4:          {
   5:              if (CloseChild != null)
   6:              {
   7:                  CloseChild(sender, e);
   8:              }
   9:   
  10:          }
  11:   
  12:  private void button1_Click(object sender, RoutedEventArgs e)
  13:          {
  14:              if (CloseChild != null)
  15:              {
  16:                  CloseChild(null, null);
  17:              }
  18:          }

Nun kann unsere ChildWindow Klasse zuerst das UserControl instanzieren, in den XAML Tree der Mainpage integrieren und das Closing Event abwarten um dort das UserControl wieder zu entfernen.

Die ChildWindow Klasse definiert die Show Methode der universell ein UserControl als Parameter übergeben wird. Was normalerweise mit einem show+= reicht um ein Event zu registrieren muss nun ob des unbekannten Typs per Reflection gelöst werden.

Ab Zeile 11 wird ein wenig UI erzeugt um zb einen grauen Hintergrund zu haben und damit das modale Verhalten. Diesem Border Ui Element wird das UserControl zum anlegen eines neuen Kunden hinzugefügt.

Ein wenig tricky ist der Zugriff auf den umgebenden Container um diesen neuen Part einzubinden. Da man nicht weis ob es sich um Grid, Canvas oder Stackpanel handelt, wird per Schlüsselwort Dynamic der Compiler davon überzeugt Zeile 18 zu kompilieren, obwohl zu dem Zeitpunkt nicht klar ist ob es wirklich eine Children Collection als Eigenschaft gibt.

   1:  class childwnd : Window
   2:      {
   3:          public void show(UserControl uc)
   4:          {
   5:   
   6:              System.Reflection.EventInfo eventInfo = (uc.GetType()).GetEvent("CloseChild");
   7:              Delegate handler = Delegate.CreateDelegate(eventInfo.EventHandlerType, this, "CloseChild");
   8:   
   9:              eventInfo.AddEventHandler(uc,handler);
  10:             
  11:              Border brd = new Border();
  12:              var clr = new SolidColorBrush();
  13:              clr.Color = Color.FromArgb(128, 178, 178, 178);
  14:              brd.Background = clr;
  15:              brd.Child = uc;
  16:              Window parent = App.Current.MainWindow;
  17:              dynamic g = parent.Content;
  18:              g.Children.Add(brd);
  19:   
  20:          }

Das Ergebnis sieht, wie ich finde, reichlich cool aus, hat aber noch Potential nach oben.

image

Wenn der Benutzer speichern oder Abbrechen drückt wird analog die Rolle Rückwärts geprobt und das Usercontrol wieder entfernt.

   1:     private void CloseChild(object sender, EventArgs e)
   2:          {
   3:              Window parent = App.Current.MainWindow;
   4:              dynamic g = parent.Content;
   5:              if (sender != null)
   6:              {
   7:                  var btn = sender as Button;
   8:                  btn.Command.Execute(btn.CommandParameter);
   9:              }
  10:              g.Children.RemoveAt(g.Children.Count - 1);
  11:   

 

Allerdings klappt in meinen Beispiel der Aufruf der SaveCustomer Methode (gebunden über SaveCustomerCommand) nicht. Als Workaround dienen Zeile 5-9, die explizit das Command aufrufen, das im Binding deklariert wurde. Der Switch zwischen Abbruch und Speichern geschieht über den Sender. Dies wird im UserControl direkt gesteuert.

Der Vollständigkeit halber noch der C# Source Code des Viewmodels basierend auf der Northwind Datenbank gemappt mit dem Entity Framework.

   1:    public void SaveCustomer()
   2:          {
   3:              var c = new Customers();
   4:              c.CustomerID = newCustomer.CustomerID;
   5:              c.CompanyName = newCustomer.CompanyName;
   6:              c.ContactName = newCustomer.ContactName;
   7:              CustomersList.Add(c);
   8:              db.Customers.Add(c);
   9:              db.SaveChanges();
  10:          }

Die Bindbare Eigenschaft für das Command, unter Nutzung einer Hilfsklasse RelayCommand.

   1:  public ObservableCollection<Customers> CustomersList { get; set; }
   2:  public Customers newCustomer { get; set; }
   3:   
   4:  public ICommand SaveCustomerCommand { get { return new RelayCommand(SaveCustomer); } }
   5:       
   6:        

Dies stellt nur ein Konzept dar und erfordert sicherlich weiteren Aufwand für eine komplette Implementierung.

Kommentare sind geschlossen