Sonntag, 10. Juni 2012

MVVM - Dynamic ViewModel

Wer Desktop-Applikationen für Windows programmiert wird sich bestimmt mit dem inzwischen auch nicht mehr neustem Framework WPF auseinandergesetzt haben. Dieses Framework bietet einen eleganten Weg die Backend-Funktionalität sauber von der graphischen Oberfläche abzukoppeln. Wie genau das geschieht ist im MVVM-Entwurfsmuster spezifiziert. So besteht das Programm im Groben aus 3 Teilen:
  1. Model (zB. Database-Objects)
  2. ViewModel
  3. View
Die Kommunikation zwischen ViewModel und Model findet über sogennante Bindings statt. Damit das aber auch reibungslos funktioniert, sind einige Mechanismen nötig, welche das ViewModel zu implementieren hat. In der Regel sind das das INotifyPropertyChanged-Interface und die Commands.
Jedoch erschwert die Implementierung oft die Arbeit und können auch den Code unleserlich machen. Das kann dann soweit führen, dass man für jedes Model eine Wrapper-Klasse erstellt.

Eine Ausführliche Erklärung des MVVM Patterns ist in diesem sehr zu empfehlenden Artikel zu finden:
Aber das .Net 4.0-Framework schafft Abhilfe. Es ermöglicht nämlich eine dynamische Programmierung. Mit diesem Feature ist es nun möglich diese Mechanismen für die entsprechenden ViewModels automatisch zu erzeugen. Dies ist übrigens auch WPF Version 4 zu verdanken:
WPF supports data binding to objects that implement IDynamicMetaObjectProvider. For example, if you create a dynamic object that inherits from DynamicObject in code, you can use markup extension to bind to the object in XAML. For more information, see the Binding Sources Overview.
(Quelle: http://msdn.microsoft.com/en-us/library/bb613588(VS.100).aspx#binding)

Damit der nachfolgende Artikel nachvollziehbar ist, solltet ihr euch auch einen Artikel über die DLR ansehen:
Wie in den Artikeln zu lesen, können Klassen geschaffen werden, deren Properties (sogar Methoden) dynamisch erstellt werden. Das machen wir uns zu nutze um das INotifyPropertyChange-Interface dynamisch zu  wrappen. Außerdem wäre es nicht schlecht, wenn man im ViewModel die Commands bequem über Attributes definieren könnte und sich somit die Initialisierung der ICommand-Properties spart. Ich habe mir das ungefähr so vorgestellt:
public class MainViewModel
{
 public MainViewModel() { }
 
 public IList<User> UserCollection { get; set; }
 public IList<Article> ArticleCollection { get; set; }
 
 public string Title { get; set; }
 
 [Command("AddArticleCommand", "CanAddArticle", 
         typeof(MainViewModel), new String[] {"Title"})]
 public void AddArticleCommandExecute(object objArticle)
 {
  Article article = objArticle as Article;
  Console.WriteLine("Add article " + article.Name);
  Title = article.Name;
 }
 
 public bool CanAddArticle(object objArticle) 
 {
  return objArticle != null;
 }
 
 [Command]
 public void HelpCommandExecute(object obj)
 {
  Console.WriteLine("Here you get some help.");
 }
}
Zu sehen ist die Klasse MainViewModel mit 3 Properties und 3 Methoden. Der Wrapper soll nun dafür sorgen, dass beim setzen eines Properties das PropertyChangedEvent ausgelöst wird. Außerdem sollen ICommand-Properties für jede Methode, die mit einem Command-Attribut versehen ist dynamisch erzeugt werden.

Schritt 1: INotifyPropertyChanged

Beginnen wir also mit der Implementierung der Wrapper-Klasse. Ich habe hier eine generische Klasse genommen. Das hat den Vorteil, dass man aufwendigen Reflection-Operationen für einen Typen nur einmal aufwenden muss. Diesen Teil der Arbeit habe ich nämlich in einen Static-Konstruktor gepackt:
public sealed class DynamicViewModel<T> : DynamicObject, INotifyPropertyChanged
{
 private static readonly SortedDictionary<string, PropertyInfo> properties = new ...;
 
 static DynamicViewModel()
 {
  // init all properties
  var props = typeof(T)
   .GetProperties()
   .Where(p => p.CanRead);
  
  foreach (var p in props)
   properties.Add(p.Name, p);
 }
        ...
}

Wie im Code zu sehen, werden alle Readable Properties des Types T abgefragt und in eine Map gespeichert. Diese Map benöigen wir jetzt für das Setzen und Zurückgeben der veimeindlichen Properties. Hierfür überschreiben wir die zwei Methoden der Klasse DynamicObject: TryGetMember, TrySetMember:
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
 PropertyInfo propInf = null;
 
 if (properties.TryGetValue(binder.Name, out propInf))
 {
  result = propInf.GetValue(instance, null);
 }
 else
 {
  // if debug mode do some extra stuff.
  result = null;
  return false;
 }
 
 return true;
}

public override bool TrySetMember(SetMemberBinder binder, object value)
{
 PropertyInfo propInf = null;
 
 if (properties.TryGetValue(binder.Name, out propInf) && propInf.CanWrite)
 {
  propInf.SetValue(instance, value, null);
  OnPropertyChanged(propInf.Name); /* Hier wird das PropertyChangedEvent ausgelöst */
  
  return true;
 }
 else
 { 
  // if debug mode do some extra stuff. 
  return false;
 }
}
Und siehe da, jetzt wird beim setzen der Properties automatisch das PropertyChangedEvent ausgelöst (Zeile 26):
dynamic viewModel = new DynamicViewModel<MainViewModel>(new MainViewModel(), true);

PropertyChangedEventHandler handler = (s, e) => 
   Console.WriteLine("Property Changed: " + e.PropertyName);
viewModel.PropertyChanged += handler;

viewModel.Title = "Neuer Titel!";

// Ausgabe: Property Changed: Title

Das ist schonmal nicht schlecht. Aber jetzt kommen wir zum etwas komplizierteren Teil: Die dynamische Erstellung der Commands.


Schritt 2: Commands

In WPF übergibt man die Funtkionalität des ViewModels über sogenannte Commands an die View. Commands sind Klassen, die das Interface ICommand implementieren. Das Inteface ist folgendermaßen definiert:
public interface ICommand
{
 void Execute(object param);
 bool CanExecute(object param);

 event EventHandler CanExecuteChanged
}
Die Aufgabe des Wrappers ist es nun, der View Properties des Typs ICommand für die Methoden, die mit einem CommandAttribut versehen sind, zur Verüfung zu stellen. Das CommandAttribute beinhaltet Informationen über den CommandName, der CanExeucte-MethodInfo und den betroffenen Properties, welche von dem Command verändert werden. Die Klasse sieht wie folgt aus:
public class CommandAttribute : Attribute
{
 private string canExecuteName;
 private Type type;
 
 public CommandAttribute()
 {
 }
 
 public CommandAttribute(string commandName, String canExecuteName = null, 
    Type type = null, String[] affectedProps = null)
 {
  if (canExecuteName != null && type == null)
    throw new ArgumentNullException("type");
  
  CommandName = commandName;
  AffectedProperties = affectedProps;
  this.canExecuteName = canExecuteName;
  this.type = type;
 }
 
 public CommandAttribute(String canExecuteName, Type type, 
    String[] affectedProps = null)
 {
  AffectedProperties = affectedProps;
  this.canExecuteName = canExecuteName;
  this.type = type;
 }
 
 public CommandAttribute(string commandName, String[] affectedProps)
 {
  CommandName = commandName;
  AffectedProperties = affectedProps;
 }
 public string CommandName { get; internal set; }
 
 public String[] AffectedProperties { get; private set; }
 
 public MethodInfo CanExecuteMethodInfo
 {
  get
  {
   if (canExecuteName != null)
    return type.GetMethod(canExecuteName); 
   else
    return null;
  }
 }
}
Nun müssen wir den Statischen Konstruktor von DynamicViewModel<T> erweitern, um die Methoden in eine Map zu speichern:
private static readonly List<Tuple<CommandAttribute, MethodInfo>> commands = ... ;

static DynamicViewModel()
{
 /* 
  * Init the properties...
  */


 // init all methods
 var methods = typeof(T)
  .GetMethods()
  .Select(m=>Tuple.Create(m.GetCustomAttributes(typeof(CommandAttribute),false),m))
  .Where(m => m.Item1.Length > 0)
  .Select(m => Tuple.Create(m.Item1.First() as CommandAttribute, m.Item2));

 foreach (var m in methods)
 {
  var mthdAttr = m.Item1;
  var mthdInf = m.Item2;
  
  // if there is no command name, get name over convention
  if (mthdAttr.CommandName == null || mthdAttr.CommandName == string.Empty) {
   int i = mthdInf.Name.IndexOf("Command") + "Command".Length;
   if (i != -1)
    mthdAttr.CommandName = mthdInf.Name.Substring(0, i);
   else throw new ArgumentException("The method attribute has no name!", 
         "CommandName");
  }
  
  commands.Add(m);
 }
}
Jetzt haben wir zwar die MethodInfo´s, jedoch müssen noch die ICommand-Properties erzeugt werden. Hierfür benötigen wir leider eine Instanz des ViewModels (schließlich handelt sich sich ja um eine Methode und nicht um eine Funktion). Deshalb geschieht dieser Schritt im "normalen" Konstruktor:
private readonly SortedDictionary<string, ICommand> propertyCommands = ...;

public DynamicViewModel(T instance, bool addMethods = false)
{ 
 this.instance = instance;
 
 if (addMethods) foreach (var cmd in commands)
 {
  var mthdAttr = cmd.Item1;
  var mthdExecInf = cmd.Item2;
  var mthdCanExecInf = mthdAttr.CanExecuteMethodInfo;
  
  Action<object> execute = (Action<object>) Delegate.CreateDelegate(
    typeof(Action<object>), 
    instance, mthdExecInf);

  Action<object> finalExecute = p => {
   execute(p);
   if (mthdAttr.AffectedProperties != null) 
   {
    foreach (var str in mthdAttr.AffectedProperties) 
     OnPropertyChanged(str);
   }
  };
  
  Predicate<object> canExecute = null;
  if (mthdCanExecInf != null)
  {
   canExecute = (Predicate<object>) Delegate.CreateDelegate(
    typeof(Predicate<object>), 
    instance, mthdCanExecInf);
  }
  else
  {
   canExecute = p => true;
  }
  
  propertyCommands.Add(mthdAttr.CommandName, new CommandImpl(finalExecute, canExecute));
 }
}
Auffallend ist vielleicht die Funktion Delegate.CreateDelegate. Hier werden (wie der Name verrät) zwei Delegates für die Instanz instance erzeugt: canExecute und execute. Das Delegate execute wird in Zeile 15 um die Fähigkeit erweitert, durch das Delegate (bzw. Methode) veränderte Properties den Event-Abonnenten über das PropertyChangedEvent zu melden. Anschließend wird in der Map propertyCommands ein neuer Eintrag mit dem Namen des Commands als Key und dem Command, welches aus den beiden Delegates gebildet wird, als Value hinzugefügt. Zu guter Letzt muss noch die TryGetMember-Methode erweitert werden:
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
 PropertyInfo propInf = null;
 ICommand command = null;
 
 if (properties.TryGetValue(binder.Name, out propInf))
 {
  result = propInf.GetValue(instance, null);
 }
 else if (propertyCommands.TryGetValue(binder.Name, out command))
 {
  result = command;
 }
 else
 {
  // if debug mode do some extra stuff.
  result = null;
  return false;
 }
 
 return true;
}
Fertig ist der DynamicViewModel-Wrapper. Natürlich kann dieser Wrapper noch an einigen Stellen verbessert werden, die Idee sollte aber nun klar sein.

Inspiriert wurde ich von folgendem MSDN-Artikel:
Der Download des Beispielcodes ist hier zu finden: Datei.

Für Anregungen und Kritik wäre ich wie immer dakbar ;)

Keine Kommentare: