Wednesday, June 29, 2016

Another way to implement a binding-computable properties in WPF

For example, there is a project in the WPF and the ViewModel in it, in which there are two properties of Price and Quantity, and computable property TotalPrice = Price * Quantity

Code
public class Order: BaseViewModel
    {
        private double _price;
        private double _quantity;
        public double Price
        {
            get {return _price; }
            set
            {
                if (_price == value)
                    return;
                _price = value;
                RaisePropertyChanged ( "Price");
            }
        }
        public double Quantity
        {
            get {return _quantity; }
            set
            {
                if (_quantity == value)
                    return;
                _quantity = value;
                RaisePropertyChanged ( "Quantity");
            }
        }
        public double TotalPrice {get {return Price * Quantity; }}
    }
    public class BaseViewModel: INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void RaisePropertyChanged (string propertyName)
        {
            var propertyChanged = PropertyChanged;
            if (propertyChanged! = null)
                propertyChanged (this, new PropertyChangedEventArgs (propertyName));
        }
    }


If Price will be changed in the code, the price changes are automatically displayed in the View, because ViewModel View report of Price Change by calling the event RaisePropertyChanged ( «Price»). Computed TotalPrice did not change in the View, because no one is RaisePropertyChanged ( «TotalPrice»). You can call RaisePropertyChanged ( «TotalPrice») in the same places where called RaisePropertyChanged ( «Price») and RaisePropertyChanged ( «Quantity»), but do not want to spread over a plurality of information on places that TotalPrice depends on Price and Quantity, and I would like to store this information in one place. To this end, a variety of people write dependency management, but let's see what is the minimum code is actually needed for this.




The standard way prokinut logic to where she did not place in terms of design - this event. The approach on the forehead is the creation of two events OnPriceChanged and OnQuantityChanged. When triggered, these events do RaisePropertyChanged ( «TotalPrice»). Make a subscription to these events in the constructor of the ViewModel. After that, the information that TotalPrice depends on Price and Quantity will be in one place - in the constructor (well, or in a separate method, if you so choose).

Few simplify the problem: we already have the PropertyChanged event, activated for Change Price, and here is his use.

        public void RegisterPropertiesDependencies (string propertyName, List <string> dependenciesProperties)
        {
            foreach (var dependencyProperty in dependenciesProperties)
            {
                this.PropertyChanged + = (sender, args) =>
                {
                    if (args.PropertyName == dependencyProperty) RaisePropertyChanged (propertyName);
                };
            }
        }
        RegisterPropertiesDependencies ( "TotalPrice", new List <string> { "Price", "Quantity"});


This code has several drawbacks: First, I would not recommend to sew up the property names in the line, it is better to get them out of the lambdas, and secondly, this code will not work if the calculated property is more complicated, for example: TotalCost = o .OrderProperties.Orders.Sum (o => o.Price * o.Quantity).

Code OrderProperties and ViewModel. Here everything is clear, you can not watch

Subscribe via events to changes Price and Quantity of each item in the collection. But in the collection can be added \ removed elements. If you change the collection you want to call RaisePropertyChanged ( «TotalPrice»). When you add an item to subscribe to his change of Price and Quantity. More should be noted that in OrderProperties someone can assign a new collection or a new ViewModel OrderProperties.

The result here is a code:

        public void RegisterElementPropertyDependencies (string propertyName, object element, ICollection <string> destinationPropertyNames, Action actionOnChanged = null)
        {
            if (element == null)
                return;
            if (actionOnChanged! = null)
                actionOnChanged ();
            if (element is INotifyPropertyChanged == false)
                throw new Exception (string.Format ( "It is impossible to keep track of changes when bindings in {0}, because it does not implement the INotifyPropertyChanged", element.GetType ()));
            ((INotifyPropertyChanged) element) .PropertyChanged + = (o, eventArgs) =>
            {
                if (destinationPropertyNames.Contains (eventArgs.PropertyName))
                {
                    RaisePropertyChanged (propertyName);
                    if (actionOnChanged! = null)
                        actionOnChanged ();
                }
            };
        }
        public void RegisterCollectionPropertyDependencies <T> (string propertyName, ObservableCollection <T> collection, ICollection <string> destinationPropertyNames, Action actionOnChanged = null)
        {
            if (collection == null)
                return;
            if (actionOnChanged! = null)
                actionOnChanged ();
            foreach (var element in collection)
            {
                RegisterElementPropertyDependencies (propertyName, element, destinationPropertyNames);
            }
            collection.CollectionChanged + = (sender, args) =>
            {
                RaisePropertyChanged (propertyName);
                if (args.NewItems! = null)
                {
                    foreach (var addedItem in args.NewItems)
                    {
                        RegisterElementPropertyDependencies (propertyName, addedItem, destinationPropertyNames, actionOnChanged);
                    }
                }
            };
        }


In this case, for OrderProperties.Orders.Sum (o => o.Price * o.Quantity) it should be used like this:

RegisterElementPropertyDependencies ( "Summa", this, new [] { "OrderProperties"},
                () => RegisterElementPropertyDependencies ( "Summa", OrderProperties, new [] { "Orders"},
                () => RegisterCollectionPropertyDependencies ( "Summa", OrderProperties.Orders, new [] { "Price", "Quantity"})));


To test this code in different situations: Quantity changed from the elements, creating new Orders and OrderProperties, me first and then Quantity Orders, etc., the code worked correctly.

P.S. By the way, I recommend to look in the direction in Observables Knockout style. There's no need to specify what determines the property, you just need to transfer the algorithm for its calculation:
fullName = new ComputedValue (() => FirstName.Value + "" + ToUpper (LastName.Value));
The library will analyze the expression tree, sees in him access to members of the FirstName and the LastName, and she will check dependencies. Fading Risk forget pereukazat depending upon changes in the algorithm for calculating the properties. True, they say that the library is a bit of a work in progress and does not track the nested collection, but if you have a car free time, you can open the source code (available on the previous link) and a bit of work with a file, or write your own bike analyzer expression tree.

P.P.S. With regard to garbage collection: if you add finalizers elements display of messages, you'll find that when you close the window all the elements collected by the garbage collector (despite the fact that the ViewModel has a reference to the child element and the child element has a reference to the ViewModel in the event handler) . This is due to the fact that in WPF to eliminate memory leaks when using DataBinding-th weak events by PropertyChangedEventManager-a.

No comments:

Post a Comment