We have an application that uses WPF User Controls that we would like to dynamically change using a combobox within a pluggable architecture. The solution is based on MVVMC sometimes called MVVM+Controller.
We did this by using a SpecificViewContentProvider which as the name suggests provides a view model for a ContentPresenter that has been bound to a view model with a user control as an observable property. In our case the application is very specific therefore we did not mind coupling the View coupled to the controller. If we wanted to break this dependency we would need to use MVVMLite or Prism. Again in this case this would be an overkill, therefore we decided to leave this dependency.
We where thinking of using MVVMLite in this application and there are some traces left, for this example it is not necessary. But frameworks like Prism and MVVMLite are very useful in simplifying applications that have complex controls. The idea is that the controls produce relay events that broadcast to anyone who is interested in there status. For example a complex tree view to complex controls to view different aspects of the application
We looked at how Research would like to build the individual controls. For reasons described in day 3 we cannot inherit xaml code, therefore if we wanted to have a base control we would have to only use code behind. In windows forms this was possible with visual inheritance. Since this is not possible within xaml the visual befit of inheritance is lost. Therefore we decided to take a different approach:
Most models different sets of controls. We decided a better approach would be to setup some custom controls like CreditDecductible, ZonalDeductibles etc and compose these within the control that is to be exported into the RunModelWizard.
Here is how we achieved this:
1. We create a user control within ArchitectureExample.PresentationLayer.WPF.Common
We make some binding eg {Binding Deductible, Mode= TwoWay}
<UserControl x:Class="ArchitectureExample.PresentationLayer.WPF.Common.Controls.CreditDeductibleView"
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"
mc:Ignorable="d"
d:DesignWidth="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Content="Deductible :" Grid.Column="0" Grid.Row="0"/>
<TextBox Text="{Binding Deductible, Mode= TwoWay}" Grid.Column="1" Grid.Row="0"/>
<Label Content="Limit :" Grid.Column="0" Grid.Row="1"/>
<TextBox Text="{Binding Limit, Mode= TwoWay}" Grid.Column="1" Grid.Row="1"/>
</Grid>
</UserControl>
We don't make separate ModelViews because this would be an overkill
2. Next we make our first injectable control ModelSettingsForEarthquakeAlaskaView.xaml within the Views/ModelSettings directory of ArchitectureExample.PresentationLayer.WPF.Client
The interesting line is
<my:CreditDeductibleView Name="creditDeductibleView1" DataContext="{Binding CreditDeductible}" />
We bind the ScreditDeductible view to CreditDeducble
<UserControl x:Class="ArchitectureExample.PresentationLayer.WPF.Client.Views.ModelSettings.ModelSettingsForEarthquakeAlaskaView"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300" xmlns:my="clr-namespace:ArchitectureExample.PresentationLayer.WPF.Common.Controls;assembly=ArchitectureExample.PresentationLayer.WPF.Common">
<StackPanel>
<my:CreditDeductibleView Name="creditDeductibleView1" DataContext="{Binding CreditDeductible}" />
</StackPanel>
</UserControl>
3. In the code behind we this export this. In this way we make this available to the combo box via MEF as a dependency injection container
using System.ComponentModel.Composition;
using System.Windows.Controls;
using ArchitectureExample.BusinessLayer.Entities;
namespace ArchitectureExample.PresentationLayer.WPF.Client.Views.ModelSettings
{
/// <summary>
/// Interaction logic for ModelSettingsForModel2Control.xaml
/// </summary>
[CatModelExport(ModelType.EarthquakeAlaska, typeof(IModelSettingView))]
public partial class ModelSettingsForEarthquakeAlaskaView : UserControl, IModelSettingView
{
public ModelSettingsForEarthquakeAlaskaView()
{
InitializeComponent();
}
}
}
4. The CatModelExport attribute is defined as:
namespace ArchitectureExample.BusinessLayer.Entities
{
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class CatModelExportAttribute : ExportAttribute
{
public CatModelExportAttribute(ModelType modelType, Type type)
: base(type)
{
this.ModelType = modelType;
}
public ModelType ModelType { get; private set; }
}
}
(Here we had some problems in getting MEF to work because originally we used (Type type, ModelType modelType) resulting in nothing being exported into the container.
5. The ModelType is also defined in this assembly as
namespace ArchitectureExample.BusinessLayer.Entities
{
public enum ModelType
{
None = 0,
...
EarthquakeAlaska = 6,
...
}
}
6. This is imported in ModelSettingsView
The interesting lines are the binding that is made to the SelectedModelSetting
<ContentPresenter x:Name="SpecificModelSetting" DataContext="{Binding SelectedModelSetting}" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"/>
This is set with the combobox control
<ComboBox x:Name="cmbModel" Grid.Column="1" Grid.Row="0" ItemsSource="{Binding ModelSettings}" SelectedItem="{Binding SelectedModelSetting, Mode=TwoWay}" DisplayMemberPath="Value.Name"/>
The OK button is bound to the Command SaveModelSettingsCommand. By the way, if we where programming in Silverlight we would need MVVMLite or Prism to do this because Silverlight does not support ICommand. Actually the code needed is quite small so you could role your own, but the easiest way is to take a widely used third party library like MVVMLight
<Button x:Name="btnOk" Content="OK" Margin="0 0 5 0" Command="{Binding SaveModelSettingsCommand}" CommandParameter="{Binding SelectedModelSetting.Value}"/>
<UserControl x:Class="ArchitectureExample.PresentationLayer.WPF.Client.Views.ModelSettings.ModelSettingsView"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Content="Model" Grid.Column="0" Grid.Row="0"/>
<ComboBox x:Name="cmbModel" Grid.Column="1" Grid.Row="0" ItemsSource="{Binding ModelSettings}" SelectedItem="{Binding SelectedModelSetting, Mode=TwoWay}" DisplayMemberPath="Value.Name"/>
<ContentPresenter x:Name="SpecificModelSetting" DataContext="{Binding SelectedModelSetting}" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"/>
<StackPanel Orientation="Horizontal" Grid.Column="1" Grid.Row="2" HorizontalAlignment="Right">
<Button x:Name="btnOk" Content="OK" Margin="0 0 5 0" Command="{Binding SaveModelSettingsCommand}" CommandParameter="{Binding SelectedModelSetting.Value}"/>
<Button x:Name="btnCancel" Content="Cancel"/>
</StackPanel>
</Grid>
</UserControl>
7. The code behind this imports the ViewFactory
namespace ArchitectureExample.PresentationLayer.WPF.Client.Views.ModelSettings
{
[Export(typeof(ModelSettingsView))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public partial class ModelSettingsView : UserControl
{
private ModelSettingsController controller;
[Import(typeof(ViewFactory))]
public ViewFactory ViewFactory { get; set; }
[Import]
[SuppressMessage("Microsoft.Design", "CA1044:PropertiesShouldNotBeWriteOnly", Justification = "Needs to be a property to be composed by MEF")]
public ModelSettingsViewModel ViewModel
{
set
{
if (this.DataContext != value)
{
this.DataContext = value;
controller = new ModelSettingsController(ViewFactory, value, this);
}
}
}
public ModelSettingsView()
{
InitializeComponent();
}
}
}
8. The View Factory contains and enumerable of Lazy<IModelSettingView, IModelSettingMetadata>
namespace ArchitectureExample.PresentationLayer.WPF.Client.Infrastructure
{
public class ViewFactory
{
[ImportMany(typeof(IModelSettingView))]
public IEnumerable<Lazy<IModelSettingView, IModelSettingMetadata>> ModuleSettingsViews { get; set; }
private readonly CompositionContainer container;
public ViewFactory(CompositionContainer container)
{
this.container = container;
}
public T GetView<T>(string viewName)
{
var view = this.container.GetExportedValueOrDefault<T>(viewName);
if (view == null)
{
throw new InvalidOperationException(string.Format("Unable to locate view with name {0}.", viewName));
}
return view;
}
public IModelSettingView GetView(ModelType modelType)
{
//var view = this.container.GetExportedValues<Lazy<T, TMetaData>>().Where<Lazy<T, TMetaData>>(a => a.Metadata.ModelType == modelType).First();
var view = ModuleSettingsViews.Where(a => a.Metadata.ModelType == modelType).First();
if (view == null)
{
throw new InvalidOperationException(string.Format("Unable to locate view with type {0}.", modelType));
}
return view.Value;
}
public T GetView<T>()
{
var view = this.container.GetExportedValueOrDefault<T>();
if (view == null)
{
throw new InvalidOperationException(string.Format("Unable to locate view with type {0}.", typeof(T).Name));
}
return view;
}
public Window CreateWindow<T>()
{
var view = this.GetView<T>();
return CreateWindow<T>(view); ;
}
public Window CreateWindow<T>(T content)
{
var window = new Window();
// window.DataContext = dataContext;
window.Content = content;
return window;
}
public ModelSettingsViewWithRegions CreateModelSettingsViewWithRegions()
{
IRegionManager regionManager = this.container.GetExportedValueOrDefault<IRegionManager>();
ModelSettingsViewWithRegions view = new ModelSettingsViewWithRegions();// { DataContext = viewModel };
this.container.ComposeParts(view);
RegionManager.SetRegionManager(view, regionManager);
RegionManager.UpdateRegions();
return view;
}
}
}
Lets take a closer look ate the
[ImportMany(typeof(IModelSettingView))]
public IEnumerable<Lazy<IModelSettingView, IModelSettingMetadata>> ModuleSettingsViews { get; set; }
ImportMany asks MEF to populate the IEnumerable with exported objects that implement IModelSettingView
If we take a look into the defintion of Lazy we see...
[Serializable]
public class Lazy<T, TMetadata> : Lazy<T>
...
//
// Summary:
// Initializes a new instance of the System.Lazy<T,TMetadata> class with the
// specified metadata that uses the specified function to get the referenced
// object.
//
// Parameters:
// valueFactory:
// A function that returns the referenced object.
//
// metadata:
// The metadata associated with the referenced object.
[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
public Lazy(Func<T> valueFactory, TMetadata metadata);
...
So what this is saying is that this function provides an interface to the meta data that is added in our Attribute.
In our case the interface to the meta data is IModelSettingMetadata
namespace ArchitectureExample.BusinessLayer.Entities
{
public interface IModelSettingMetadata
{
ModelType ModelType { get; }
}
}
This meta data is needed to identify the control we want to import. Within the function GetView we use a linq expression to find the modelview that we are interested in. Since this is an enumeration of type lazy we need to use .value to instantiate the view model.
public IModelSettingView GetView(ModelType modelType)
{
var view = ModuleSettingsViews.Where(a => a.Metadata.ModelType == modelType).First();
if (view == null)
{
throw new InvalidOperationException(string.Format("Unable to locate view with type {0}.", modelType));
}
return view.Value;
}
This is used in the ModelSettings Controller...
9. Here is the ModelSettingsController used in the MVVMC pattern
namespace ArchitectureExample.PresentationLayer.WPF.Client.Views.ModelSettings
{
public class ModelSettingsController
{
private ViewFactory viewFactory;
private ModelSettingsViewModel viewModel;
private ModelSettingsView view;
public ModelSettingsController(ViewFactory viewFactory,ModelSettingsViewModel viewModel, ModelSettingsView view)
{
this.viewFactory = viewFactory;
this.viewModel = viewModel;
this.view = view;
//this.view.DataContext = viewModel;
//This listens to events created when the combobox changes
this.viewModel.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(viewModel_PropertyChanged);
UpdateSpecificModelSettingsView();
}
void viewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "SelectedModelSetting")
{
UpdateSpecificModelSettingsView();
}
}
private void UpdateSpecificModelSettingsView()
{
if (this.viewModel.SelectedModelSetting != null)
{
//==Here is where the Imported Enumeration of Lazy IModelSettingView are used
var specificView = viewFactory.GetView(this.viewModel.SelectedModelSetting.Metadata.ModelType) as UserControl;
//Here we have to set the data context because we are changing the control
specificView.DataContext = this.viewModel.SelectedModelSetting.Value;
//Here we populate the place holder with our view (tightly coupled)
this.view.SpecificModelSetting.Content = specificView;
}
}
}
}
10. The View model belonging to ModelSettingsView.xaml is ModelSettingsViewModel.cs
Inside this we expose Lazy Observable collections and selected values. Here we are using MVVMLite and the ViewModelBase. Actually we don't need to do this because WPF has an implementation of ICommand.
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
namespace ArchitectureExample.PresentationLayer.WPF.Client.Views.ModelSettings
{
[Export(typeof(ModelSettingsViewModel))]
//[PartCreationPolicy(CreationPolicy.NonShared)]
public class ModelSettingsViewModel : ViewModelBase
{
private IModelSettingsService moduleSettingsService;
private ObservableCollection<Lazy<ModelSettingsBase, IModelSettingMetadata>> modelSettings;
private Lazy<ModelSettingsBase, IModelSettingMetadata> selectedModelSetting;
private RelayCommand<ModelSettingsBase> saveModelSettingsCommand;
public ObservableCollection<Lazy<ModelSettingsBase, IModelSettingMetadata>> ModelSettings
{
get
{
return modelSettings;
}
private set
{
if (modelSettings != value)
{
modelSettings = value;
RaisePropertyChanged("ModelSettings");
}
}
}
public Lazy<ModelSettingsBase, IModelSettingMetadata> SelectedModelSetting
{
get
{
return selectedModelSetting;
}
set
{
if (selectedModelSetting != value)
{
selectedModelSetting = value;
RaisePropertyChanged("SelectedModelSetting");
SaveModelSettingsCommand.RaiseCanExecuteChanged();
}
}
}
public RelayCommand<ModelSettingsBase> SaveModelSettingsCommand
{
get
{
if (saveModelSettingsCommand == null)
{
saveModelSettingsCommand = new RelayCommand<ModelSettingsBase>((ms) =>
{
// Save
ms.Save();
}
, (ms) =>
{
// CanSave
return ms !=null && ms.Validate();
});
}
return saveModelSettingsCommand;
}
}
[ImportingConstructor]
public ModelSettingsViewModel(IModelSettingsService moduleSettingsService)
{
this.moduleSettingsService = moduleSettingsService;
ModelSettings = new ObservableCollection<Lazy<ModelSettingsBase, IModelSettingMetadata>>(this.moduleSettingsService.GetModuleSettings());
foreach (Lazy<ModelSettingsBase, IModelSettingMetadata> modelsettings in ModelSettings)
{
var test= modelsettings.Value;
}
}
}
}
The interesting parts of the above are
public RelayCommand<ModelSettingsBase> SaveModelSettingsCommand
Notice that in the can Execute Func we first chack if Ms !=null. Without this check first the function will throw an exception
, (ms) =>
{
// CanSave
return ms !=null && ms.Validate();
});
The ms.Validate part is invoking an implementation of an abstract class:
namespace ArchitectureExample.BusinessLayer.Entities
{
/// <summary>
/// This classes could be generated from a T4 template (pocos)
/// </summary>
public abstract class ModelSettingsBase
{
/// <summary>
/// Just an example of a field
/// </summary>
public string Name
{
get { return this.GetType().Name; }
}
public abstract void Save();
public abstract bool Validate();
}
}
11. Here is where we have implemented this within
namespace ArchitectureExample.BusinessLayer.Entities
{
public class CreditDeductible : ModelSettingsBase
{
public double Limit { get; set; }
public double Deductible { get; set; }
public CreditDeductible()
{
Limit = 100;
Deductible = 0;
}
public override void Save()
{
}
public override bool Validate()
{
return Limit <= 100 && Limit >= 0 && Deductible <= 100 && Deductible >= 0;
}
}
}
Notice that we decided to use a boolean to return validation. The alternative would have been to use an exception. We choose boolean because exceptions are slow because they need to serialize the stack trace.
There is a validation enterprise building block that uses attributes to set validation rules. We will look into using this because it provides a tidy way to associate validation rules to properties and will remove the untidy code in the Validate() function
We tested this with 2 imported controls with 1 and 2 of these user controls. The OK button of the main control was properly enabled and disabled according to the validation rules. Also as we changed the combo box the values from one custom control to another where correctly persisted...