Quantcast
Channel: MVVM – Dean Chalk
Viewing all articles
Browse latest Browse all 26

Simple Property Rules Engine With MVVM

$
0
0

I’m currently working on a large legacy WPF project where my ViewModels often have a large number of properties, and those properties have some complex inter-relationships that need to be reflected in the behaviour of the app, via the ViewModels and XAML bindings.

Specifically, some properties may need to support some of the following behaviours:

1) A property may need to invoke a change notification in the UI if a related property changes

2) A property may be a calculated value, and the calculation may rely on other property values which may change.

3) A property may need to support validation

With these requirements in mind, I wrote a simple POC (proof of concept) of a ‘rules engine’ that may provide those features and can be utilised in the ViewModel in a straightforward way.

Below is the code for the rules engine class:

using System;
using System.Collections.Generic;
using System.ComponentModel;

public class PropertyRuleEngine
{
    private readonly Dictionary<string, List<string>> dependencies = 
        new Dictionary<string, List<string>>();
    private readonly Dictionary<string, Action> evaluations = 
        new Dictionary<string, Action>();
    private readonly Dictionary<string, List<Func<string>>> validations = 
        new Dictionary<string, List<Func<string>>>();
    private readonly object source;

    public PropertyRuleEngine(object source)
    {
        this.source = source;
    }

    public PropertyRuleEngine AddDependency(string dependentPropertyName, 
        string propertyName)
    {
        if (!this.dependencies.ContainsKey(propertyName))
        {
            this.dependencies.Add(propertyName, new List<string>());
        }

        if (!this.dependencies[propertyName].Contains(dependentPropertyName))
        {
            this.dependencies[propertyName].Add(dependentPropertyName);
        }

        return this;
    }

    public PropertyRuleEngine AddEvaluatedProperty(string propertyName, 
        Action calculateAction)
    {
        this.evaluations.Add(propertyName, calculateAction);
        return this;
    }

    public PropertyRuleEngine AddValidationProperty(string propertyName, 
        Func<string> validationFunction)
    {
        if (!this.validations.ContainsKey(propertyName))
        {
            this.validations.Add(propertyName, new List<Func<string>>());
        }

        this.validations[propertyName].Add(validationFunction);
        return this;
    }

    public void Notify(string property, PropertyChangedEventHandler handler)
    {
        this.InvokePropertyChangedHandler(property, handler);
        if (!this.dependencies.ContainsKey(property))
        {
            return;
        }

        foreach (string tmp in this.dependencies[property])
        {
            if (this.evaluations.ContainsKey(tmp))
            {
                this.evaluations[tmp]();
            }
            else
            {
                this.InvokePropertyChangedHandler(tmp, handler);
            }
        }
    }

    public IEnumerable<string> GetErrors(string property)
    {
        List<string> result = null;
        if (!this.validations.ContainsKey(property))
        {
            return result;
        }

        foreach (var validation in validations[property])
        {
            var tmp = validation();
            if (!string.IsNullOrEmpty(tmp))
            {
                if (result == null)
                {
                    result = new List<string>();
                }
                result.Add(tmp);
            }
        }
        return result;
    }

    private void InvokePropertyChangedHandler(string propertyName, 
        PropertyChangedEventHandler originalHandler)
    {
        PropertyChangedEventHandler handler = originalHandler;
        if (handler == null)
        {
            return;
        }

        Delegate[] delegates = handler.GetInvocationList();
        foreach (Delegate d in delegates)
        {
            d.DynamicInvoke(new[] { this.source, 
                new PropertyChangedEventArgs(propertyName) });
        }
    }
}

And here is the (rather simplistic) test ViewModel that utilises the rules engine. It demonstrates how it handles inter-property dependencies, calculated properties and validation

using System;
using System.ComponentModel;

public class TestDomainObjectViewModel : INotifyPropertyChanged, IDataErrorInfo
{
    private readonly PropertyRuleEngine rulesEngine;
    public event PropertyChangedEventHandler PropertyChanged;

    public TestDomainObjectViewModel()
    {
        this.rulesEngine = new PropertyRuleEngine(this);
        this.rulesEngine
            .AddDependency("TestName", "TestId")
            .AddEvaluatedProperty("TestName", () => 
                this.TestName = string.Format("Name : {0}", 
                                this.TestId.ToString()))
            .AddValidationProperty("TestId", () => 
                    this.TestId > 50 ? "Number too big" : string.Empty);
    }

    private string testName;
    public string TestName
    {
        get
        {
            return testName;
        }
        set
        {
            testName = value;
            this.rulesEngine.Notify("TestName", this.PropertyChanged);
        }
    }

    private int testId;
    public int TestId
    {
        get
        {
            return this.testId;
        }
        set
        {
            this.testId = value;
            this.rulesEngine.Notify("TestId", this.PropertyChanged);
        }
    }
        
    public string this[string columnName]
    {
        get
        {
            var result = rulesEngine.GetErrors(columnName);
            if (result == null)
            {
                return null;
            }
            return string.Join(Environment.NewLine, result);
        }
    }

    public string Error
    {
        get
        {
            return null;
        }
    }
}

And finally, here is the XAML and (if your new to MVVM) the code-behind file for completeness

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <TextBlock Text="ID" />
    <TextBox Text="{Binding TestId, Mode=TwoWay, 
        ValidatesOnDataErrors=True,
        UpdateSourceTrigger=LostFocus}" Grid.Column="1" 
        Width="100">
        <TextBox.Style>
            <Style TargetType="TextBox">
                <Style.Triggers>
                    <Trigger Property="Validation.HasError" Value="True">
                        <Setter Property="ToolTip" 
                            Value="{Binding RelativeSource=
                            {RelativeSource Mode=Self}, 
                            Path=(Validation.Errors)[0].ErrorContent}" />
                    </Trigger>
                </Style.Triggers>
            </Style>
        </TextBox.Style>
    </TextBox>
    <TextBlock Text="Name" Grid.Row="1" />
    <TextBox Text="{Binding TestName, Mode=TwoWay}" Grid.Row="1" Grid.Column="1" 
                Width="100" IsReadOnly="True" />
    <Button Content="Update" Grid.Row="2" />
</Grid>
public MainWindow()
{
    InitializeComponent();
    DataContext = new TestDomainObjectViewModel();
}

Viewing all articles
Browse latest Browse all 26

Trending Articles