WAQS: Add a bindable dynamic property

7 reasons to use WAQS

WAQS documentation

 

This feature is only usable with WPF. It does not work with PCL.

MVVM wrappers are a pain

Imagine a master detail with suppliers as master and products as details.

In this master detail, we want to be able to select many masters using a CheckListBox. When we check/uncheck a supplier, details list must be refreshed.

 

How to do it?

First, adding a property Checked on Supplier entity is a bad idea IMHO.

 

An often used way to do it with MVVM consist in using a wrapper which encapsulates Supplier class and adds a Checked property. This property set raises an event. The ViewModel adds handler on its event and refreshes the products list when it is raised.

This code is not very complex but is not very interesting to write:

public class SupplierViewModel  
{  
   
public Supplier Supplier { get; set; }  

   
private bool _isSelected;  
    public bool IsSelected  
 
   {  
       
get { return _isSelected; }  
       
set  
        {  
           
if (value == _isSelected)  
               
return;  
            _isSelected =
value;  
            if (IsSelectedChanged != null)   
                IsSelectedChanged();   
        }  
    }  

   
public event Action IsSelectedChanged;  
}  
public class MainWindowViewModel : ViewModelBase
{
    private INorthwindClientContext _context;


    public MainWindowViewModel(INorthwindClientContext context)
        : base(context)
    {
        _context = context;
    }


    private IEnumerable<SupplierViewModel> _suppliers;
    private bool _suppliersLoaded;
    public IEnumerable<SupplierViewModel> Suppliers
    {
        get
        {
            if (!_suppliersLoaded)
            {
                _suppliersLoaded = true;
                LoadSuppliersAsync().ConfigureAwait(true);
            }
            return _suppliers;
        }
        private set
        {
            _suppliers = value;
            NotifyPropertyChanged.RaisePropertyChanged(() => Suppliers);
        }
    }

    private async Task LoadSuppliersAsync()
    {
        Suppliers = (await _context.Suppliers.AsAsyncQueryable().ExecuteAsync()).Select(s =>
            {
               
var supplierViewModel = new SupplierViewModel { Supplier = s };
                supplierViewModel.IsSelectedChanged += SupplierViewModelIsSelectedChanged;
               
return supplierViewModel;
            }).ToList();
    }

    private IEnumerable<Product> _products;
    public IEnumerable<Product> Products
    {
        get { return _products; }
        private set
        {
            _products = value;
            NotifyPropertyChanged.RaisePropertyChanged(() => Products);
        }
    }


    private void SupplierViewModelIsSelectedChanged()
    {
        LoadProductsAsync().ConfigureAwait(
true);
    }


private int _loadProductsIndex = 0; 
    private async Task LoadProductsAsync()
    {
int loadProductsIndex = ++_loadProductsIndex;
        var selectedSuppliers = Suppliers.Where(s => s.IsSelected).Select(s => s.Supplier).ToList();
        if (selectedSuppliers.Count == 0)
            Products = Enumerable.Empty<Product>();
        else
        {
            var supplierProducts = await _context.Products.AsAsyncQueryable().Where(p => selectedSuppliers.Contains(p.Supplier)).ExecuteAsync();
if (_loadProductsIndex == loadProductsIndex)

Products = supplierProducts;

        }
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing && _suppliers != null)
            foreach (var s in _suppliers)
                s.IsSelectedChanged -= SupplierViewModelIsSelectedChanged;
        base.Dispose(disposing);
    }
}


 



Note that this code would be more complex if we allow masters addition or removal because, in this case, we also have to manage collections synchronization.



 



PropertyDescriptor



In WPF, when we bind a control property on an object property, .NET does not directly use the property.



Actually, .NET binds the control with a PropertyDescriptor.



By default, we have one PropertyDescriptor by property but we can override it implementing ICustomTypeDescriptor interface.



 



WAQS WPF entities implement this interface.



In addition, WAQS adds some helpers for PropertyDescriptor usage with an IDynamicType interface (implemented by entities on client):



public interface IDynamicType
{
    IEnumerable<PropertyDescriptor> GetCustomPropertyDescriptors();
    void AddPropertyDescriptor(PropertyDescriptor propertyDescriptor);
    void AddOrReplacePropertyDescriptor(PropertyDescriptor propertyDescriptor);
    void RemovePropertyDescriptor(PropertyDescriptor propertyDescriptor);
    void RemovePropertyDescriptor(string propertyName);
}









So instead of using a wrapper, we can just add a PropertyDescriptor in Supplier type using the following code:



public class MainWindowViewModel : ViewModelBase
{
    private INorthwindClientContext _context;

   
public MainWindowViewModel(INorthwindClientContext context)
        : base(context)
    {
        _context = context;
    }

    private List<Supplier> _selectedSuppliers = new List<Supplier>();

    private IEnumerable<Supplier> _suppliers;
    private bool _suppliersLoaded;
    public IEnumerable<Supplier> Suppliers
    {
        get
        {
            if (!_suppliersLoaded)
            {
                _suppliersLoaded = true;
                LoadSuppliersAsync().ConfigureAwait(true);
            }
            return _suppliers;
        }
        private set
        {
            _suppliers = value;
            NotifyPropertyChanged.RaisePropertyChanged(() => Suppliers);
        }
    }

    private async Task LoadSuppliersAsync()
    {
        Suppliers = (await _context.Suppliers.AsAsyncQueryable().ExecuteAsync()).Select(s =>
            {
                ((IDynamicType)s).AddPropertyDescriptor(new DynamicType<Supplier>.CustomPropertyDescriptor<bool>("IsSelected",
                    s2 => _selectedSuppliers.Contains(s2),
                    (s2, value) =>
                    {
                        if (value)
                        {
                            if (!_selectedSuppliers.Contains(s))
                            {
                                _selectedSuppliers.Add(s);
                                LoadProductsAsync().ConfigureAwait(true);
                            }
                        }
                        else if (_selectedSuppliers.Contains(s))
                        {
                            _selectedSuppliers.Remove(s);
                            LoadProductsAsync().ConfigureAwait(true);
                        }
                    }));
                return s;
            }).ToList();
    }

    private IEnumerable<Product> _products;
    public IEnumerable<Product> Products
    {
        get { return _products; }
        private set
        {
            _products = value;
            NotifyPropertyChanged.RaisePropertyChanged(() => Products);
        }
    }

    private int _loadProductsIndex = 0; 
    private async Task LoadProductsAsync()     {       int loadProductsIndex = ++_loadProductsIndex;         var selectedSuppliers = Suppliers.Where(s => s.IsSelected).Select(s => s.Supplier).ToList();         if (selectedSuppliers.Count == 0)             Products = Enumerable.Empty<Product>();         else         {             var supplierProducts = await _context.Products.AsAsyncQueryable().Where(p => selectedSuppliers.Contains(p.Supplier)).ExecuteAsync();             if (_loadProductsIndex == loadProductsIndex)                 Products = supplierProducts;         }     }
 
    protected override void Dispose(bool disposing)     {        if (disposing && _suppliers != null)            foreach (var sin _suppliers)  
               ((IDynamicType)s).RemovePropertyDescriptor("IsSelected");   



       base.Dispose(disposing);
   }
}


 



In order to simplify this, we can add or remove PropertyDescriptor on the WAQS client context directly instead of doing it for each entities.



public class MainWindowViewModel : ViewModelBase
{
    private INorthwindClientContext _context;


    public MainWindowViewModel(INorthwindClientContext context)
        : base(context)
    {
        _context = context;
        _context.AddProperty<Supplier, bool>("IsSelected", s => _selectedSuppliers.Contains(s), (s, value) =>
        {
            var selectedSuppliers = _selectedSuppliers;
            if (value)
            {
                if (!selectedSuppliers.Contains(s))
                {
                    selectedSuppliers.Add(s);
                    LoadProductsAsync().ConfigureAwait(true);
                }
            }
            else if (selectedSuppliers.Contains(s))
            {
                selectedSuppliers.Remove(s);
                LoadProductsAsync().ConfigureAwait(true);
            }
        });
    }

    private List<Supplier> _selectedSuppliers = new List<Supplier>();

    private IEnumerable<Supplier> _suppliers;
    private bool _suppliersLoaded;
    public IEnumerable<Supplier> Suppliers
    {
        get
        {
            if (!_suppliersLoaded)
            {
                _suppliersLoaded = true;
                LoadSuppliersAsync().ConfigureAwait(true);
            }
            return _suppliers;
        }
        private set
        {
            _suppliers = value;
            NotifyPropertyChanged.RaisePropertyChanged(() => Suppliers);
        }
    }

    private int _loadProductsIndex = 0; 
    private async Task LoadProductsAsync()     {         int loadProductsIndex = ++_loadProductsIndex;         var selectedSuppliers = Suppliers.Where(s => s.IsSelected).Select(s => s.Supplier).ToList();         if (selectedSuppliers.Count == 0)             Products = Enumerable.Empty<Product>();         else         {             var supplierProducts = await _context.Products.AsAsyncQueryable().Where(p => selectedSuppliers.Contains(p.Supplier)).ExecuteAsync();             if (_loadProductsIndex == loadProductsIndex)                 Products = supplierProducts;         }     } 
 
 
    private IEnumerable<Product> _products;
    public IEnumerable<Product> Products     {        get {return _products; }        private set 
       { 
           _products =value
           NotifyPropertyChanged.RaisePropertyChanged(() => Products); 
       }     }
    private async Task LoadProductsAsync()
    {         Products =await _context.Products.AsAsyncQueryable().Where(p => _selectedSuppliers.Contains(p.Supplier)).ExecuteAsync();    } }


In this case, ViewModelBase Dispose method disposes the context that removes entities PropertyDescriptor added from itself.



 



Note that in order to optimizes it, because Suppliers – Products is a one to many relationship, we could replace the basic LoadProducts call by this code:



_context.AddProperty<Supplier, bool>("IsSelected",
    s => _selectedSuppliers.Contains(s),
    (s, value) =>
    {
        var selectedSuppliers = _selectedSuppliers;
        if (value)
        {
            if (!selectedSuppliers.Contains(s))
            {
                selectedSuppliers.Add(s); 
                AddProducts(s).ConfigureAwait(true); 
            }
        }
        else if (selectedSuppliers.Contains(s))
        {
            selectedSuppliers.Remove(s); 
            Products = Products.Except(s.Products); 
        }
    });









 



With this AddProducts method:



private HashSet<Supplier> _alreadyLoaded = new HashSet<Supplier>();  
private async Task AddProducts(Supplier s)
{
    if (_alreadyLoaded.Contains(s))
    {
        Products = Products.Union(s.Products);
        return;
    }
    await s.LoadProductsAsync();
    if (_selectedSuppliers.Contains(s))
    {
        if (_alreadyLoaded.Contains(s))
            return;
        if (Products == null)
        {
            Products = s.Products;
            return;
        }
        Products = Products.Union(s.Products);
    }
    _alreadyLoaded.Add(s);
}
This entry was posted in 16868, 8708. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>