If you’ve developed a WPF or Silverlight application, you’ve likely used the MVVM pattern. You likely also encountered issues implementing some functionality under MVVM. Some mechanisms are difficult to implement without moving away from MVVM. What’s more, MVVM limitations may not relate to any particular control because the WPF/Silverlight platform itself has no full support for MVVM development. The typical problems with MVVM in WPF/Silverlight are well-known. In fact, several frameworks were specifically introduced to address these issues: PRISM, MVVM Light, Caliburn, etc. These frameworks can work in conjunction with DevExpress components as with standard components.
If several solutions already exist, why would DevExpress offer yet another MVVM framework?
The answer is, the better part of our customers use the MVVM pattern (including third party MVVM frameworks); we have a clear picture of the challenging scenarios in MVVM development. We can address issues solely within our components, but sometimes issues are related to the MVVM framework.
With an MVVM library of our own, MVVM capabilities are exposed at the component level.The end-result is a holistic solution for a well-designed MVVM application whose parts fit together perfectly.
In this series of posts we’ll discuss the following features provided by DevExpress MVVM Framework:
- Commands
- Basic ViewModel classes
- Attached behaviors
- Services
- Value converters
- Messaging and loosely-coupled MVVM architecture
The MVVM functionality at the DevExpress.Xpf.Mvvm library has no external dependencies, so you can use it in the view model part of your project without referencing a UI library.
Let’s review the commanding mechanism and ViewModel types. While you may find this subject familiar, the details are specific to our framework.
The MVVM library provides two implementations of the ICommand interface:
- DelegateCommand defining a parameterless command;
- DelegateCommand<T> defining a single parameter command of the parametrized type.
Both commands support two constructors: a constructor accepting Execute delegate; a second constructor accepting Execute and CanExecute delegates:
1: DelegateCommand delegateCommand =
2:new DelegateCommand(() => MessageBox.Show("This is a DelegateCommand);
3: DelegateCommand<string> delegateCommand =
4:new DelegateCommand<string>(x => MessageBox.Show(x), x => !string.IsNullOrEmpty(x));
A DelegateCommand<T> automatically converts the command argument to the parameterized type if possible. For example, a CommandParameter string is converted to the parametrized type:
1:<ButtonCommand="{Binding ShowDocumentCommand}"CommandParameter="Text"/>
1:publicenum DocumentType { Text, Data }
2: DelegateCommand<DocumentType> ShowDocumentCommand =
3:new DelegateCommand<DocumentType>(OnShowDocumentCommandExecute);
4:
5:void OnShowDocumentCommandExecute(DocumentType parameter) {
6:if(parameter == DocumentType.Text) {
7:///
8: }
9:if(parameter == DocumentType.Data) {
10:///
11: }
12: }
An optional constructor parameter (for WPF only) specifies whether a DelegateCommand uses the CommandManager to raise the CanExecuteChanged event. By default useCommandManager is true, making it unnecessary to manually implement disabled/enabled logic for your commands. You can just set the CanExecute delegate for the command and the delegate is automatically triggered when an end-user interacts with the UI. Note that when the CommandManager is used for this purpose, the CanExecute handler is called frequently, so avoid performing time-consuming operations in the delegate.
If you pass false as the last constructor argument, the CommandManager is not used. In this case, update your command by calling the RaiseCanExecuteChanged method.
1: DelegateCommand GoBackCommand =
2:new DelegateCommand(OnGoBackCommandExecute, OnGoBackCommandCanExecute, false);
3: DelegateCommand GoForwardCommand =
4:new DelegateCommand(OnGoForwardCommandExecute, OnGoForwardCommandCanExecute, false);
5:void OnGoBackCommandExecute() {
6:///
7: UpdateCommandsState();
8: }
9:void OnGoForwardCommandExecute() {
10:///
11: UpdateCommandsState();
12: }
13:bool OnGoBackCommandCanExecute() {
14:///
15: }
16:bool OnGoForwardCommandCanExecute() {
17:///
18: }
19:void UpdateCommandsState() {
20: GoBackCommand.RaiseCanExecuteChanged();
21: GoForwardCommand.RaiseCanExecuteChanged();
22: }
A DelegateCommands can process an event in the view layer using the EventToCommand class. The following is a simple example binding a command to an event:
1:<Window ...>
2:<dxmvvm:Interaction.Triggers>
3:<dxmvvm:EventToCommandCommand="{Binding InitializeCommand}"EventName="Loaded"/>
4:</dxmvvm:Interaction.Triggers>
5: ...
6:</Window>
The EventToCommand class allows passing event arguments as a command argument. It’s importantly to note in most cases event arguments are a part of the View layer, and it’s necessary to convert event arguments to an object that belonging to the ViewModel. EventToCommand provides an EventArgsConverter property for this purpose.
1:<ListBox ...>
2:<dxmvvm:Interaction.Triggers>
3:<dxmvvm:EventToCommandEventName="MouseDoubleClick"
4:Command="{Binding ItemDoubleClickCommand}"
5:PassEventArgsToCommand="True">
6:<dxmvvm:EventToCommand.EventArgsConverter>
7:<Helpers:ListBoxDoubleClickEventArgsConverter/>
8:</dxmvvm:EventToCommand.EventArgsConverter>
9:</dxmvvm:EventToCommand>
10:</dxmvvm:Interaction.Triggers>
11:</ListBox>
1:publicclass ListBoxDoubleClickEventArgsConverter : EventArgsConverterBase<MouseButtonEventArgs> {
2:protectedoverrideobject Convert(MouseButtonEventArgs args) {
3:///
4: }
5: }
Some DevExpress components provide ready-to-use event argument converters. For instance, the EventArgsToDataRowConverter and EventArgsToDataCellConverter converters can be used with the GridControl. These converters pass a data row object or information about a cell to a command.
1:<dxg:GridControl ... >
2:<dxmvvm:Interaction.Triggers>
3:<dxmvvm:EventToCommandCommand="{Binding EditCommand}"
4:EventName="MouseDoubleClick"
5:PassEventArgsToCommand="True">
6:<dxmvvm:EventToCommand.EventArgsConverter>
7:<dx:EventArgsToDataRowConverter/>
8:</dxmvvm:EventToCommand.EventArgsConverter>
9:</dxmvvm:EventToCommand>
10:</dxmvvm:Interaction.Triggers>
11:</dxg:GridControl>
Let’s review the classes for ViewModel creation.
BindableBase is a simple implementation of the INotifyPropertyChanged interface with two additional methods for implementing view model properties: SetProperty and RaisePropertyChanged. The following example demonstrates simple and complex scenarios for these methods.
1:publicclass BindableObject : BindableBase {
2:string stringProperty1;
3:publicstring StringProperty1 {
4: get { return stringProperty1; }
5: set { SetProperty(ref stringProperty1, value, () => StringProperty1); }
6: }
7:string stringProperty2;
8:publicstring StringProperty2 {
9: get { return stringProperty2; }
10: set { SetProperty(ref stringProperty2, value, () => StringProperty2); }
11: }
12:
13:string stringProperty3;
14:publicstring StringProperty3 {
15: get { return stringProperty3; }
16: set {
17:if(SetProperty(ref stringProperty3, value, () => StringProperty3)) {
18: RaisePropertiesChanged(() => StringProperty1, () => StringProperty2);
19: }
20: }
21: }
22: }
Notice we’ve declared lambda expressions returning properties. This approach is very useful because it allows code checking during compilation and easy property renaming. However, in rare cases, application performance is degraded when a property is frequently updated. To accommodate these scenarios, BindableBase provides a static GetPropertyName method to calculate property names once from the ViewModel static constructor.
1:publicclass BindableObject : BindableBase {
2:staticstring StringProperty1Name = string.Empty;
3:static BindableObject() {
4: BindableObject obj = null;
5: StringProperty1Name = BindableBase.GetPropertyName(() => obj.StringProperty1);
6: }
7:
8:string stringProperty1;
9:publicstring StringProperty1 {
10: get { return stringProperty1; }
11: set { SetProperty(ref stringProperty1, value, StringProperty1Name); }
12: }
13: }
The BindableBase class only provides basic capabilities for implementing bindable objects, so you’ll likely use ViewModelBase as the base class for your base ViewModel. ViewModelBase descends from BindableBase and offers additional capabilities which may prove helpful. For instance, you can initialize ViewModel parameters at design-time. Set properties by overriding the OnInitializeInDesignMode method:
1:publicclass ViewModel : ViewModelBase {
2:string stringProperty1;
3:publicstring StringProperty1 {
4: get { return stringProperty1; }
5: set { SetProperty(ref stringProperty1, value, () => StringProperty1); }
6: }
7:protectedoverridevoid OnInitializeInDesignMode() {
8:base.OnInitializeInDesignMode();
9: StringProperty1 = "TestString";
10: }
11: }
In complex applications, you may opt for design-time and runtime ViewModel registration via dependency injection. In simple cases, however, overriding OnInitializeInDesignMode is useful.
ViewModelBase also implements several interfaces: ISupportParentViewModel, ISupportServices, ISupportParameter. These interfaces are used by the service mechanism, to be discussed in upcoming posts.
Thank you for your time. See you!