Introduction
This article is part of a series about how to dynamically generate parts of the ViewModel layer when working with the Model-View-ViewModel pattern in WPF. The first article can be found at the link below:
Dynamic View Model
Background
Consider the following problem: You require a list of objects on the parent that the user can dynamically edit. We will call these objects options. At some depth below the parent there is another list of children that contains a key representing an option. How do you allow the user to select the option from a ComboBox when editing a child such that when the option list updates, the ComboBox items update as well, while only storing a single primitive key on the child?
The primary technique I will describe in this article answers this question within the context of WPF. I also introduce some other improvements and upgrades to the DynamicVM library, including a ProjectManager class.
Pros, Cons, and Updates
Pros:
- Improves programmer productivity by eliminating the time spent writing ViewModel classes.
- When the Model layer changes, no updates are needed for the ViewModel layer.
- Undo, Add, Remove, MoveUp, MoveDown, and shared selection are supported out-of-the-box.
- Provides a clear separation between the domain data and interface data.
- The model only needs to implement INotifyPropertyChanged. The model is not required to derive from a specific class and it is not assumed to contain any interface data.
Cons:
- While data is represented well using this technique, methods are not. Any custom functions required at the ViewModel layer will require a specialized derived class. (#1 Below)
- If you don't want some of the features provided by the ViewModel layer, it is not possible to turn them off for a specific model.
- This library contains no built-in data validation systems.
Updates:
- This new version of DynamicVM has a new class which facilitates custom functions at the ViewModel layer, ViewModelFactory.
- Supports foreign key-like references between children and options at a higher level in the logical tree with minimal coding.
- Includes a project manager which handles the serialization of XML objects and the construction of the entire ViewModel layer. The ProjectManager class supports boilerplate commands such as New, Save, etc. and listens to file and device changes (such as a flash drive being pulled out).
- Supports unique id constraints on integer properties if specified.
- Supports direct children via the ViewModelDirectChild class, which behaves exactly like ViewModelProperty except that it can host objects which implement INotifyPropertyChanged.
The Techniques
A new Attribute has been added, KeyRefAttribute, which is used to decorate the "foreign key" property on a child object:
[KeyRef("Options", typeof(ParentModel), "Xuid", typeof(OptionModel), "OptionRef")]
public int OptionKey
{
get
{
return m_optionKey;
}
set
{
m_optionKey = value;
NotifyPropertyChanged("OptionKey");
}
}
The attribute expects five parameters in the constructor:
- The name of the list property on the parent object which contains the objects to select from.
- The type of the parent object.
- The name of the "primary key" property on the target object in the list.
- The type of the target object.
- The name of the ViewModelProperty item that is generated by the system. This name is visible to Xaml for the purpose of binding in a DataTemplate.
These attributes are identified and loaded by the KeyRefInitializationStage class during the construction of the ViewModelManager in ProjectManager. In the LoadProperties method of DynamicViewModel, any properties found in the KeyRefUtility object will cause an additional ViewModelProperty object to be generated. The extra object, of type ViewModelPropertyRef, contains a new property, Source, which exposes the parent list to Xaml.
public ViewModelCollection Source
{
get
{
DynamicViewModel parent = m_parentUtility[m_keyRefAttribute.ParentType.FullName];
return parent.GetMemberByName(m_keyRefAttribute.ParentRefListName) as ViewModelCollection;
}
}
The source get accessor walks up the tree using the ViewModelParentUtility indexer and finds the target list host. It then gets the ViewModelCollection that wraps the target list. This allows a ComboBox to bind the SelectedItem property to "OptionRef" in our above example.
If you recall from the first article, the CachedValue property on the ViewModelProperty class is accessed by DynamicViewModel in the TryGetMember method for non-INotifyPropertyChanged types. This property is overloaded in our new class:
public override object CachedValue
{
get
{
return base.CachedValue;
}
set
{
if (NotEqual(value))
{
object key = value as DynamicViewModel != null ? m_refXuidInfo.GetValue((value as DynamicViewModel).ModelContext) : null;
object oldKey = m_cachedValue as DynamicViewModel != null ? m_refXuidInfo.GetValue((m_cachedValue as DynamicViewModel).ModelContext) : null;
m_manager.UndoManager.Do(new SetPropertyCommand(m_modelContext, key, oldKey, m_info.Name));
}
}
}
Thus ViewModelPropertyRef can be thought of as a special case of ViewModelProperty that stores a reference to a DynamicViewModel instead of a reference to a primitive.
Now consider the following problem: imagine that you have two properties at the root model level, a list of options and a list of children. If the options are loaded first, then the individual DynamicViewModel targets will be visible to PropertyChanged and CollectionChanged listeners. But if the children are loaded before the options, the logic of wiring up the appropriate listeners becomes more complicated. To get around this issue, we simply ignore the default reflection data provided by the system and substitute our own sorted list of properties.
The class we use to create this new list of properties is called ViewModelKeyRefUtility. It has a method for sorting the properties so they will be loaded in the correct order to greatly simplify the process of wiring up listeners between the child and option objects.
DynamicViewModel then requests reflection data by calling a method on the ViewModelKeyRefUtility in place of a call to Type.GetProperties()
var reflectedProperties = m_manager.KeyRefUtility.GetPropertiesSortedByDependencies(m_modelContext.GetType());
The UidManager class has two methods, LogUidFrom and SetUidOn, which are called by the CreateModel method on ViewModelFactory. ViewModelFactory itself is a simple dictionary that is used to associate Model types with ViewModel types, allowing you to specify custom functionality and commands. These two classes, along with ProjectManager, are relatively naive and will not be covered in detail.
Using the code
ProjectManager can be added as the DataContext of your window to gain access to the associated commands and initialization features as follows:
public MainWindow()
{
InitializeComponent();
var factory = new ViewModelFactory()
{
{ typeof(OptionModel), typeof(OptionViewModel) }
};
ProjectManager<ParentModel> projectManager = new ProjectManager<ParentModel>(Dispatcher, factory);
projectManager.New();
DataContext = projectManager;
}
To wire up the manager to listen for window messages, include the following code in your window as well:
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
ProjectManager<ParentModel> currentContext = DataContext as ProjectManager<ParentModel>;
HwndSource source = PresentationSource.FromVisual(this) as HwndSource;
source.AddHook(currentContext.WndProc);
}
To wire up Key/Ref pairs, simply use the KeyRefAttribute as mentioned in the previous section:
[KeyRef("Options", typeof(ParentModel), "Xuid", typeof(OptionModel), "OptionRef")]
Then write Xaml to bind to OptionRef and OptionRef.Source for SelectedItem and ItemsSource, respectively:
<ComboBox ItemsSource="{Binding Path=OptionRef.Source, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" SelectedItem="{Binding Path=OptionRef.CachedValue, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
To specify that an integer field should be unique and auto-generated, decorate the property with the UniqueConstraint attribute and pass in the initial id:
[UniqueConstraint(0)]
public int Xuid
{
get
{
return m_xuid;
}
set
{
m_xuid = value;
NotifyPropertyChanged("Xuid");
}
}
To add functionality to a class derived from DynamicViewModel, simply add properties that return standard ICommand objects. In this example the user can click a button to remove all references to an option:
public ICommand ClearReferencesCommand
{
get
{
return new RelayCommand(ClearReferences, CanClearReferences);
}
}
private bool CanClearReferences(object arg)
{
var parent = FindParent.GetRoot().ModelContext as ParentModel;
var optionModel = ModelContext as OptionModel;
foreach(var child in parent.Children)
{
if(child.OptionKey == optionModel.Xuid)
{
return true;
}
}
return false;
}
private void ClearReferences(object obj)
{
ComplexCommand complexCommand = new ComplexCommand();
var parent = FindParent.GetRoot().ModelContext as ParentModel;
var optionModel = ModelContext as OptionModel;
foreach (var child in parent.Children)
{
if (child.OptionKey == optionModel.Xuid)
{
complexCommand.Add(new SetPropertyCommand(child, -1, child.OptionKey, "OptionKey"));
}
}
Manager.UndoManager.Do(complexCommand);
}
Associate the derived class with the model type as shown in the MainWindow constructor example above, then the command will become visible to Xaml.
<Button Height="16" Width="16" HorizontalAlignment="Center" VerticalAlignment="Center" Command="{Binding Path=ClearReferencesCommand}"/>
Points of Interest
This library can be used to create very simple tools very quickly. I have used it to develop applications for AAA game studios and in some cases development took less than a day. You only need to define your data, and the DynamicViewModel class will inject a great deal of boilerplate functionality.
This library can also be used as a reference for common WPF tasks such as sharing visual state between ViewModel objects, listening for device changes, and safely loading XML files without losing data using the SerializationBase class in DynamicVM.IO. This version also contains the initial workings of a drag/drop system.
While testing the library I discovered that while foreign keys work for objects in other lists, objects in the same list are a bit more difficult to handle. I am using a class called InstanceDependencyManager to create a hidden list of model objects that can sorted and then loaded in the correct order to prevent missing dependencies. However, these children need access to objects in the parent list as well, so DynamicViewModel objects need to be created and inserted into the list in the wrong order, at least during initialization. This creates a problem if other systems happen to be listening to CollectionChanged events during this stage.
To solve that problem I changed ObservableList to override OnCollectionChanged as follows:
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!m_suppressChanges)
{
base.OnCollectionChanged(e);
}
}
This allows me to suppress the CollectionChanged event while I am inserting items into the list in the wrong order. I also had to modify the LoadProperties method in DynamicViewModel to initialize ViewModelCollection objects after creation, also to prevent a specific crash caused when users try to use KeyRefAttribute to reference a key on the same class.
History
Initial article. 7/19/2014
Fixed several crashes and functionality issues. KeyRef can now be used to reference a key on the same class. 7/27/2014
Future Work
I plan to add an async operations library that will allow the UI to bind to an output log as tasks are being completed. As such I have made an effort to make some of the classes in this library thread-safe. I also plan to add more Drag/Drop features.
If you have used the library, you have probably noticed that the code expects all objects in the ViewModel tree to be of types defined within DynamicVM. In the future I want to separate functionality into interfaces so that users can insert their own custom, non-DynamicViewModel objects directly into the logical tree.