WPF/SL: lazy loading TreeView

01/26/2012: Code update

Imagine the following scenario: you have a WCF service with two methods:

List<Customer> GetCustomers();
List<Order> GetOrders(int CustomerId);


You want a treeview with lazy loading in a WPF Window.



There is many way to do it.



I identify three main in my searches:



  • you can use event on your treeview implemented in code-behind
  • you can makes your TreeView control inheriting the framework one’s
  • you can use all the logic on ViewModels and use binding


The last point is realized by adding a CustomerViewModel, having a collection of CustomerViewModel in the VM that encapsulated a Customer and adding IsExpanded property and add the logic of loading orders.



It’s a way often saw in the web and that seems a good way with MVVM for many developers but I think, IMHO, it is NOT a good way.



Indeed, what happens if under Orders, I want OrderDetails? You will add a new OrderViewModel class that encapsulates an Order and the CustomerViewModel class will have a collection of OrderViewModel?



I don’t want to make again my Model in my ViewModel.



I could use the ICustomTypeDescriptor (ICustomTypeProvider in SL) as I did here but I think that if this solution is interesting to add business logic on entity, it is not to add control logic.



I think that the lazy loading control logic should be encapsulated in a behavior and the ViewModel should just have the lazy loading WCF calls logic.



So, I use an ILazyLoader interface:



public interface ILazyLoader


{


    string GetChildPropertyName(object obj);



    bool IsLoaded(object obj);


    void Load(object obj);

}

and an implementation of it using delegate:



public class LazyLoader : ILazyLoader


{


    private Func<object, string> _getChildPropertyName;


    private Func<object, bool> _isLoaded;


    private Action<object> _load;


 


    public LazyLoader(Func<object, string> getChildPropertyName, Func<object, bool> isLoaded, Action<object> load)


    {


        _getChildPropertyName = getChildPropertyName;


        _isLoaded = isLoaded;


        _load = load;


    }


 


    public string GetChildPropertyName(object obj)


    {


        return _getChildPropertyName(obj);


    }


 


    public bool IsLoaded(object obj)


    {


        return _isLoaded(obj);


    }


 


    public void Load(object obj)


    {


        _load(obj);


    }

}

Then, in my ViewModel, I use the following code:



public class CustomerViewModel


{


    private ObservableCollection<Customer> _customers;


    public ObservableCollection<Customer> Customers


    {


        get


        {


            if (_customers == null)


            {


                _customers = new ObservableCollection<Customer>();


                var customersService = new CustomerServiceClient();


                EventHandler<GetCustomersCompletedEventArgs> serviceGetCustomersCompleted = null;


                serviceGetCustomersCompleted = (sender, e) =>


                    {


                        customersService.GetCustomersCompleted -= serviceGetCustomersCompleted;


                        foreach (var ht in e.Result)


                            _customers.Add(ht);


                    };


                customersService.GetCustomersCompleted += serviceGetCustomersCompleted;


                customersService.GetCustomersAsync();


            }


            return _customers;


        }


    }


 


    private ILazyLoader _lazyLoader;


    public ILazyLoader LazyLoader


    {


        get { return _lazyLoader ?? (_lazyLoader = new LazyLoader(obj => 


            {


                if (obj is HardwareType)


                    return PropertyName.GetPropertyName((Expression<Func<HardwareType, object>>)(ht => ht.Hardwares));


                return null;


            }, obj => _loadedHardwareTypes.Contains((HardwareType)obj), obj => LoadHardwares((HardwareType)obj))); }

    }

 


    private List<Customer> _loadedCustomers = new List<Customer>();


    private void LoadOrders(Customer c)


    {


        var customerService = new CustomerServiceClient();


        c.Orders.Clear();


        EventHandler<GetOrdersCompletedEventArgs> serviceGetOrdersCompleted = null;


        serviceGetOrdersCompleted = (sender, e) =>


        {


            customerService.GetOrdersCompleted -= serviceGetOrdersCompleted;


            foreach (var o in e.Result)


                c.Orders.Add(o);


            _loadedCustomers.Add(c);


        };


        customerService.GetOrdersCompleted += serviceGetCustomersCompleted;


        customerService.GetOrdersAsync(c.Id);


    }

}

Now, this is the code of my behavior:



public static class LazyLoadTreeViewItemBehavior


{


    public static ILazyLoader GetLazyLoader(DependencyObject obj)


    {


        return (ILazyLoader)obj.GetValue(LazyLoaderProperty);


    }


    public static void SetLazyLoader(DependencyObject obj, ILazyLoader value)


    {


        obj.SetValue(LazyLoaderProperty, value);


    }


    public static readonly DependencyProperty LazyLoaderProperty =


        DependencyProperty.RegisterAttached("LazyLoader", typeof(ILazyLoader), typeof(LazyLoadTreeViewItemBehavior), new PropertyMetadata(ApplyingLazyLoadingLogic));


 


    private static void ApplyingLazyLoadingLogic(DependencyObject o, DependencyPropertyChangedEventArgs e)


    {


        var tvi = o as TreeViewItem;


        if (tvi == null)


            throw new InvalidOperationException();


        ILazyLoader lazyLoader= GetLazyLoader(o);


        PropertyInfo childrenProp;


        if (lazyLoader == null)


            return;


        object itemValue = tvi.DataContext;


        string childrenPropName = lazyLoader.GetChildPropertyName(itemValue);


        if (childrenPropName == null || (childrenProp = itemValue.GetType().GetProperty(childrenPropName)) == null)


            return;


        IEnumerable children = (IEnumerable)childrenProp.GetValue(itemValue, null);


        RoutedEventHandler tviExpanded = null;


        RoutedEventHandler tviUnloaded = null;


        tviExpanded = (sender, e2) =>


            {


                tvi.Expanded -= tviExpanded;


                tvi.Unloaded -= tviUnloaded;
if (!lazyLoader.IsLoaded(itemValue))


                {


                    lazyLoader.Load(itemValue);


                    tvi.Items.Clear();


                    tvi.ItemsSource = children;


                }


            };


        tviUnloaded = (sender, e2) =>


            {


                tvi.Expanded -= tviExpanded;


                tvi.Unloaded -= tviUnloaded;


            };


        if (!children.GetEnumerator().MoveNext())


        {


            tvi.ItemsSource = null;


            tvi.Items.Add(new TreeViewItem());


        }


        tvi.Expanded += tviExpanded;


        tvi.Unloaded += tviUnloaded;

    }
}

The thing very interesting with it is the fact that my behavior is not dependent of my model or my ViewModel and can be used with other lazy loading TreeViews.



To do it, I just have to apply our behavior into our TreeView, what can be done in xaml:



<TreeView ItemsSource="{Binding Customers}">


    <TreeView.ItemContainerStyle>


        <Style TargetType="{x:Type TreeViewItem}">


            <Setter Property="local:LazyLoadTreeViewItemBehavior.LazyLoader" 


                    Value="{Binding DataContext.LazyLoader, RelativeSource={RelativeSource AncestorType=local:CustomersWindow}}" />


        </Style>


    </TreeView.ItemContainerStyle>


    <TreeView.ItemTemplate>


        <HierarchicalDataTemplate>


            <HierarchicalDataTemplate.ItemTemplate>


                <DataTemplate>


                    


                </DataTemplate>


            </HierarchicalDataTemplate.ItemTemplate>


            


        </HierarchicalDataTemplate>


    </TreeView.ItemTemplate>

</TreeView>

 



I really like this way. What do you think about it?



 



Of course, I write my sample with WPF but it’s still true with SL.



 



Hope this helps…

This entry was posted in 13461, 7671, 8708. Bookmark the permalink.

2 Responses to WPF/SL: lazy loading TreeView

  1. William Boney Jr. says:

    Why not create something like a reusable LazyLoadTreeView control that derives from TreeView and accept a dependency property that implements a similar interface to ILazyLoader but with methods that returns Funcs (like LoadFunc & IsLoadedFunc). Then you’ll have the choice to create a ViewModel and implement this interface (M-V-VM) or to directly implement it in the model (M-V). And it would be much more readable!

  2. Because you can use my behavior with any TreeView which uses TreeViewItems. So if you use Telerik, DevExpress, Infragistics, etc, you can use my behavior. It’s not true if you just inherit to TreeView control.

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>