Click here to Skip to main content
14,268,557 members

Quick MVVM Development in WPF with Rascl

Rate this:
3.00 (2 votes)
Please Sign up or sign in to vote.
3.00 (2 votes)
9 Aug 2019CPOL
Want an easy way to keep models and view models in sync? Want a built in property grid and undo system? How about a color picker? Rascl may be for you!

Introduction

WPF is a super powerful framework, especially when you leverage the MVVM paradigm, but it can be a real headache (and time consuming) to write the glue to synchronize models and view models.

The Robotic Arm Software Component Library (Rascl) attempts to alleviate this issue. Rascl provides an MVVM framework through a couple of key components, including PropertyAttribute, PropertyCollection and SynchronizedCollection. With Rascl, you only need a few lines of code to keep models and view models in sync with one another. As an added bonus, you get an undo system, a property grid control, a color picker, collection editor and other components to use in your applications.

The Code

To get started, all you need to do is add a reference to the Rascl library to your project. From there, you can start adding property attributes to your model, like so:

using Rascl.Core;

namespace ControlsTest
{
    [Identifier("ItemName")]
    public class TestItemA : ModelBase
    {
        [StringProperty("Item Name")]
        public string ItemName
        {
            get
            {
                return m_itemName;
            }
            set
            {
                m_itemName = value;
                OnPropertyChanged("ItemName");
            }
        }

        [FloatProperty("Start", Min=0.0, Max=100.0)]
        public double Start
        {
            get
            {
                return m_start;
            }
            set
            {
                m_start = value;
                OnPropertyChanged("Start");
            }
        }

        private double m_start;
        private string m_itemName = "Unnamed";
    }
}

Deriving our class from ModelBase just gives us a standard INotifyPropertyChanged implementation, so although it isn't absolutely necessary, it is recommended,

The Identifier attribute for the class is optional, but allows built in controls like the collection editor to use one of the object's properties as its name for display purposes.

StringProperty is a C# attribute derived from PropertyAttribute. The only required parameter is the display name, in this case "Item Name", which is always the first parameter for any PropertyAttribute.

FloatProperty adds some additional named parameters, Min and Max. These can be used for validation, or for customizing the behavior of the control used to display the data.

There are additional property attributes for colors, files, collections, enums, and more, each one having their own default editor for displaying in the Rascl PropertyGrid control. This editor can be overridden to use your own custom editor, like so:

[FloatProperty("Start", Editor="TestDoubleTemplate", Min=0.0, Max=100.0)]

Here, we've set the Editor parameter, which allows us to choose the editor used for this property in the property grid. This can be a built in Rascl editor, or your own custom editor. You can even create editors to handle complex data types, like classes. Besides the type-specific property attributes, you can also use the base PropertyAttribute for properties that don't need any metadata (like Min and Max for floats and doubles), other than the Editor name.

The Rascl property system is opt-in, which means that any property that doesn't have a PropertyAttribute won't be included in the property system. This is important because the property system adds capabilities like undo automatically to your properties, If you want to exclude a property from the PropertyGrid, but still want to use the other features of the property system, you can use the base PropertyAttribute without setting the display name, like so:

[Property("")]   // this property will exist in the properties list, 
                 // but will not show up in the PropertyGrid

Now that we've seen how to use C# attributes to mark up our models, let's move on to synchronizing those models with our view models. Take a look at the code below:

using Rascl.Core;
using Rascl.Core.Undo;

namespace ControlsTest
{
    public class TestItemViewModel : ViewModelBase
    {
        public TestItemViewModel(ModelBase model, UndoContext undo) : base(model)
        {
            Properties = new PropertyCollection(model, undo);
        }
    }
}

That's all you need to create a view model that's synchronized with the model shown above. Of course, you may need additional functionality specific to your application, but with this much, we can already write a view to display and edit all of the data from the model, keeping the model and view model in sync.

Properties is a PropertyCollection defined in ViewModelBase that the TestItemViewModel class is derived from. You initialize Properties with the underlying model class and the current UndoContext (more on that later). This creates a list of values at the view model layer that update whenever the properties from the model layer are changed. We can bind to this list of properties from XAML or access them from codebehind using an indexer like so:

XAML:

<TextBox Text="{Binding Path=Properties[ItemName].Value}"/>

C#:

Properties["ItemName"].Value = "Rascl is here";

If we were to change the model directly, the bindings would still be updated, but we'd be bypassing the other benefits we get from the property system, like undo. That's the reason why we go through the property indexer here. If there are multiple bindings to the same property, changing once through the property indexer will update it everywhere, as you'd expect.

Often in WPF, our data is modeled in a tree-like structure, where one model class can contain one or more observable collections of other models. We generally want to maintain this structure at the view model layer, but that can be a pain. Every time a list in the model changes, whether it be an add, remove, move or clear operation, we need to do the same in the view model, and when that tree structure goes several layers deep, we often end up with copy-paste-edit code to handle all those operations in a custom way per class.

SynchronizedCollection<T> is designed to handle just that case. Take a look at the following model class:

using Rascl.Core;
using System.Collections.ObjectModel;

namespace ControlsTest
{
    [Identifier("Name")]
    public class TestData : ModelBase
    {
        public TestData()
        {
            m_testItems.Add(new TestItemA() { ItemName = "First" });
            m_testItems.Add(new TestItemB() { ItemName = "Second" });
            m_testItems.Add(new TestItemA() { ItemName = "Third" });
        }

        [Property("My Name", Editor="TestStringTemplate")]
        public string Name
        {
            get
            {
                return m_name;
            }
            set
            {
                m_name = value;
                OnPropertyChanged("Name");
            }
        }
 
        [CollectionProperty("Test Items", new System.Type[] 
                           { typeof(TestItemA), typeof(TestItemB) })]
        public ObservableCollection<ModelBase> TestItems
        {
            get
            {
                return m_testItems;
            }
            set
            {
                m_testItems = value;
                OnPropertyChanged("TestItems");
            }
        }

        private string m_name;
        private ObservableCollection<ModelBase> m_testItems = 
                                     new ObservableCollection<ModelBase>();
    }
}

As you can see here, we're using the CollectionProperty attribute to describe a list of items all derived from model base, consisting of two distinct types, TestItemA and TestItemB. It isn't necessary for the items to be derived from ModelBase or for the collection to have a PropertyAttribute for SynchronizedCollection to work, but it will allow us to edit the list of objects in the property grid. Next, let's look at the corresponding view model.

using Rascl.Core;
using Rascl.Core.Undo;

namespace ControlsTest
{
    public class TestDataViewModel : ViewModelBase
    {
        public TestDataViewModel(TestData model, UndoContext undo) : base(model)
        {
            m_undo = undo;
            Properties = new PropertyCollection(model, m_undo);
            Items = new SynchronizedCollection<TestItemViewModel>
                    (model.TestItems, m_undo, x => CreateItemViewModel(x));
        }
        
        public SynchronizedCollection<TestItemViewModel> Items
        {
            get
            {
                return m_items;
            }
            private set
            {
                m_items = value;
                OnPropertyChanged("Items");
            }
        }

        private TestItemViewModel CreateItemViewModel(object model)
        {
            if (model is TestItemA itemA)
            {
                return new TestItemViewModel(itemA, m_undo);
            }
            else if (model is TestItemB itemB)
            {
                return new TestItemViewModel(itemB, m_undo);
            }
            return null;
        }
        
        private SynchronizedCollection<TestItemViewModel> m_items;
        private UndoContext m_undo;
    }
}

In the constructor, we store the passed-in UndoContext, initialize our PropertyCollection, and create a SynchronizedCollection of TestItemViewModel. Since we've looked at the properties already, let's look deeper into SynchronizedCollection.

When we create a new SynchronizedCollection, we pass in three parameters. The first is the underlying collection that we will synchronize with, the second is the UndoContext so we can undo and redo changes to the collection, and the third is the delegate used to create new view models from newly added models, in this case, we call the CreateItemViewModel function, passing in the model (x) that was created. Having access to the model that was just created allows us to create the appropriate view model, even if the underlying collection contains models of varying types that need specific view models that have different constructors or properties to initialize.

In this case, though, we're using one view model class to represent two different models. This is possible because, using the property system, we don't need to define specific properties in the view model to keep them in sync with the model. If necessary, though, we could define a view model class per model class and keep them all in the synchronized collection, as long as the SynchronizedCollection is specialized using a common base class or interface of the view models.

We can display this hierarchy in a standard WPF TreeView using HierarchicalDataTemplate to define how to display the items, like so:

<HierarchicalDataTemplate DataType="{x:Type local:TestDataViewModel}" 

 ItemsSource="{Binding Path=Items, Mode=OneWay}">
    <TextBlock Text="{Binding Path=Properties.Identifier.Value}"/>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:TestItemViewModel}">
    <TextBlock Text="{Binding Path=Properties.Identifier.Value}"/>
</HierarchicalDataTemplate>

The ItemsSource for the TestDataViewModel parent object is bound to its Items property, which is a SynchronizedCollection of TestItemViewModel. SynchronizedCollection implements the necessary interfaces to be displayed and to notify the view when changes occur.

We're using the Identifier attribute here to display the names of the objects in the tree. We could also go through the Properties indexer, but this is an easy shortcut to the same value, and if we have models with different name properties, we don't have to differentiate between them. We can then define the TreeView and a Rascl PropertyGrid to display the items and edit their properties like so:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition/>
        <ColumnDefinition Width="Auto"/>
    </Grid.ColumnDefinitions>
    <TreeView x:Name="Tree" Grid.Column="0" MinWidth="200" 

                            ItemsSource="{Binding Path=TestDataCollection}"/>
    <Border Grid.Column="2" BorderBrush="Black" BorderThickness="1">
        <ctrls:PropertyGrid Properties=
               "{Binding ElementName=Tree, Path=SelectedItem.Properties}" Width="450" />
    </Border>
</Grid>

In the codebehind, TestDataCollection is just defined as an ObservableCollection<TestDataViewModel> and we initialize it with some sample data. The TreeView has its ItemsSource set to this collection, and the PropertyGrid is bound to the Properties of the SelectedItem in the TreeView.

Here's a look at the result:

When you want to change an existing collection in a model that has a corresponding SynchronizedCollection in the view model, go through the appropriate functions provided by the SynchronizedCollection (Add, Insert, IndexOf, Remove, RemoveAt, and Clear). This allows the undo system to automatically hook into the changes being made. Add, Insert, IndexOf, and Remove all take an object of the underlying collection type. When Add is called, for instance, it adds the passed-in object to the underlying collection, creates a new view model for the object in the synchronized collection, and stores the operation on the undo stack.

There's one more thing you'll need in order to get everything working. You'll need to include the Generic.xaml resource dictionary in App.xaml. You can also include your own custom editors as data templates there as well if you want to override the default ones. This allows the editors to be found by the name passed into the PropertyAttribute on the model properties.

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="pack://application:,,,/Rascl;
                                        component/Themes/Generic.xaml" />
        </ResourceDictionary.MergedDictionaries>

        <DataTemplate x:Key="TestDoubleTemplate" DataType="{x:Type core:Property}">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>
                <Slider Grid.Column="0" Value="{Binding Path=Value}" 

                 Minimum="{Binding Path=Metadata.Min}" 

                 Maximum="{Binding Path=Metadata.Max}" 

                 undo:UndoBehavior.AtomicChangeType="MouseOnly" />
                <TextBox Grid.Column="1" Text="{Binding Path=Value, StringFormat=N2}" 

                 Width="100"/>
            </Grid>
        </DataTemplate>

    </ResourceDictionary>
</Application.Resources>

Here. you can see how we've defined TestDoubleTemplate. We bind directly to Value, since the DataContext will be set to the property, itself. You can also see how we've bound to the Min and Max values of the FloatProperty attribute. The attribute data is stored as Metadata on the property, so we can use that as well to affect the behavior of the control right in the DataTemplate.

You can also see another interesting feature at the end of the Slider declaration. The UndoBehavior.AtomicChangeType is set to MouseOnly. When we drag the Slider, the undo system tracks the value change as a single operation, and records the results when the mouse button is released. That way, when we undo the change, we rewind all the way back to the beginning of the operation, instead of displaying incremental changes made by the user while the mouse button was down. This behavior can be applied to just about any type of click-and-drag operation.

As we've discussed, the undo system automatically hooks into the property system and the SynchronizedCollection. But you can also use it for your own custom operations. Whenever you perform an operation that you want to be able to undo, you just wrap it in an UndoableActionPair and execute it like so:

undoContext.Execute(new UndoableActionPair(new Action(() =>
{
    // Code to execute here
}),
new Action(() =>
{
    // Do the opposite here (for undo)
})));

You just need the appropriate UndoContext to execute your undo/redo pairing. For many applications, a single UndoContext is fine, but if you have a multiple document interface (MDI) design, you could have one per document, and the UndoManager.CurrentContext needs to be changed based on which document is active. Then when the user presses Ctrl+Z, the appropriate undo action is performed based on that context.

If UndoableActionPair is insufficient for your needs, you can create your own class that implements the IUndoableOperation interface, and pass an instance of it into UndoContext.Execute().

If you have custom operations that need to be handled as a single, atomic operation, so that hitting undo undoes everything at once, you can wrap all your related changes between calls to UndoContext.StartAtomicOperation() and UndoContext.EndAtomicOperation().

In your MainWindow, you can create command bindings to ApplicationCommands.Undo/Redo and handle those in codebehind like this:

private void Undo_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    e.CanExecute = Framework.Instance.UndoManager.CurrentContext.CanUndo;
    e.Handled = true;
}

private void Undo_Executed(object sender, ExecutedRoutedEventArgs e)
{
    Framework.Instance.UndoManager.CurrentContext.Undo();
    e.Handled = true;
}

private void Redo_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    e.CanExecute = Framework.Instance.UndoManager.CurrentContext.CanRedo;
    e.Handled = true;
}

private void Redo_Executed(object sender, ExecutedRoutedEventArgs e)
{
    Framework.Instance.UndoManager.CurrentContext.Redo();
    e.Handled = true;
}

You can also use UndoContext.CanUndo to determine if there are changes that need to be saved for the application, and display a hint to the user (like an asterisk next to the file name) so the user knows that they need to save. This is just an added benefit of having an undo system that tracks all data changes.

There's much more to Rascl library than the functionality described here, including several more controls, and other useful classes but we will leave those for a later article.

Points of Interest

Before developing this library, we were reinventing the wheel every time a new project came along. We weren't satisfied with the MVVM libraries that were out there. Now, we've got a great starting place to develop any app in WPF. Rascl builds on what's currently available in WPF, without replacing any of the core WPF functionality. We've used this library in several successful projects and are now releasing it because we want to share with the world what we've learned over the past 10 years.

You can download the Rascl library, sample code, and documentation for free from the link below:

History

  • 6/25/2019 - Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Dan Goodman
Founder Robotic Arm Software
United States United States
No Biography provided

Comments and Discussions

 
QuestionAnother way Pin
#realJSOP4-Aug-19 7:58
mve#realJSOP4-Aug-19 7:58 
AnswerRe: Another way Pin
Dan Goodman4-Aug-19 20:32
memberDan Goodman4-Aug-19 20:32 
AnswerRe: Another way Pin
Kevin Marois5-Aug-19 4:56
professionalKevin Marois5-Aug-19 4:56 
GeneralRe: Another way Pin
#realJSOP5-Aug-19 5:57
mve#realJSOP5-Aug-19 5:57 
GeneralRe: Another way Pin
Kevin Marois5-Aug-19 6:27
professionalKevin Marois5-Aug-19 6:27 
GeneralRe: Another way Pin
#realJSOP5-Aug-19 7:17
mve#realJSOP5-Aug-19 7:17 
GeneralRe: Another way Pin
Kevin Marois5-Aug-19 9:54
professionalKevin Marois5-Aug-19 9:54 
GeneralRe: Another way Pin
#realJSOP5-Aug-19 23:19
mve#realJSOP5-Aug-19 23:19 
GeneralRe: Another way Pin
Kevin Marois6-Aug-19 6:17
professionalKevin Marois6-Aug-19 6:17 
GeneralRe: Another way Pin
mldisibio6-Aug-19 6:10
membermldisibio6-Aug-19 6:10 
QuestionMessage Closed Pin
3-Aug-19 2:20
memberMember 145496053-Aug-19 2:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 2 Aug 2019

Tagged as

Stats

6.4K views
4 bookmarked