Search code examples
wpfxamldata-bindingmvvmobservablecollection

How can I bind to a non ObservableCollection?


Is there a way to bind directly to a Collection in the model and manually tell WPF that the binding needs refreshing without having to create an ObservableCollection for it in the viewmodel?

<ListBox ItemsSource="{Binding Position.PossibleMoves}">
...
</ListBox>

Position is my model, part of a chess library, and PossibleMoves is a Collection within it. I do not want to implement INotifyProperty changed or put ObservableCollections in a stand alone optimized library.

I want to avoid copying PossibleMoves into an ObservableCollection every time the position is updated. The data binding works on initialization but it would be handy if I could also refresh the binding at will inside the viewmodel.

Calling OnNotifyPropertyChanged("Position.PossibleMoves") from the viewmodel doesn't work because the reference to the collection itself does not change.


Solution

  • You can do this by using an attached behavior to bind a handler to an event that gets triggered in the view model. You can't bind directly to events though so you have to wrap them in a class like so:

    public class Refresher
    {
        public delegate void RefreshDelegate();
        public event RefreshDelegate Refresh;
    
        public void DoRefresh()
        {
            if (this.Refresh != null)
                this.Refresh();
        }
    }
    

    Now add an instance of that to your view model:

    public class MyViewModel
    {
        public IList<string> Items { get; set; }
        
        private Refresher _Refresher = new Refresher();
        public Refresher Refresher {get {return this._Refresher;}}
    }
    

    Next create an attached behavior that registers a delegate instance with that event and forces the listbox to refresh its binding:

    public static class RefreshBehavior
    {
        public static readonly DependencyProperty RefresherProperty = DependencyProperty.RegisterAttached(
            "Refresher",
            typeof(Refresher),
            typeof(RefreshBehavior),
            new PropertyMetadata(null, OnRefresherChange));
    
        public static void SetRefresher(DependencyObject source, Refresher value)
        {
            source.SetValue(RefresherProperty, value);
        }
    
        public static Refresher GetRefresher(DependencyObject source)
        {
            return (Refresher)source.GetValue(RefresherProperty);
        }
    
        private static void OnRefresherChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Refresher.RefreshDelegate handler = () =>
            {
                var listBox = d as ListBox;
                listBox.Items.Refresh();
            };
    
            if (e.NewValue != null)
                (e.NewValue as Refresher).Refresh += handler;
            if (e.OldValue != null)
                (e.OldValue as Refresher).Refresh -= handler;
        }
    }
    

    And finally attach it to your listbox in the xaml:

    <ListBox ItemsSource="{Binding Items}"
        local:RefreshBehavior.Refresher="{Binding Refresher}"/>
    

    That's it. Call Refresher.DoRefresh() in your view model and it will force a listbox update.

    This works but it's really hammering a square peg into a round hole. If I were you I'd do everything I could to try and do proper collection changed notification in your view model. I understand you wanting to keep ObservableCollection out of your model but there are ways to proxy change notification automatically (e.g. Castle DynamicProxy).