Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Silverlight TabControl bound to ObservableCollection<string> not updating when collection changed

Silverlight 3 app with a TabControl bound to an ObservableCollection using an IValueConverter. Initial the binding works (converter called) on app startup. Changes, Clear() or Add(), to the bound collection are not reflected in the TabControl... converter not called.

note: the bound ListBox reflects the changes to the bound collection while the TabControl does not.

Ideas?

/jhd


The XAML binding...

<UserControl.Resources>
    <local:ViewModel x:Key="TheViewModel"/>
    <local:TabConverter x:Key="TabConverter" />
</UserControl.Resources>
<StackPanel DataContext="{StaticResource TheViewModel}">
    <ListBox ItemsSource="{Binding Classnames}" />
    <controls:TabControl x:Name="TheTabControl" 
        ItemsSource="{Binding Classnames, Converter={StaticResource TabConverter}, ConverterParameter=SomeParameter}"/>
    <Button Click="Button_Click" Content="Change ObservableCollection" />
</StackPanel>

The ViewModel...

namespace DatabindingSpike
{
    public class ViewModel
    {
        private ObservableCollection<string> _classnames = new ObservableCollection<string>();

        public ViewModel()
        {
            _classnames.Add("default 1 of 2");
            _classnames.Add("default 2 of 2");
        }

        public ObservableCollection<string> Classnames
        {
            get { return _classnames; }
            set { _classnames = value; }
        }
    }
}

The converter (for completeness)...

namespace DatabindingSpike
{
    public class TabConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var source = value as ObservableCollection<string>;
            if (source == null)
                return null;

            var param = parameter as string;
            if (string.IsNullOrEmpty(param) || param != "SomeParameter")
                throw new NotImplementedException("Null or unknow parameter pasased to the tab converter");

            var tabItems = new List<TabItem>();
            foreach (string classname in source)
            {
                var tabItem = new TabItem
                                  {
                                      Header = classname,
                                      Content = new Button {Content = classname}
                                  };
                tabItems.Add(tabItem);
            }

            return tabItems;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}
like image 574
John Dhom Avatar asked Aug 18 '09 14:08

John Dhom


2 Answers

Update 8/19

The concise answer is you have to implement INotifyPropertyChanged on the view model and notify listeners when the Property/Collection is changed.

Implement INotifyPropertyChanged on the ViewModel

* implement the interface INotifyPropertyChanged
* define the event (public event PropertyChangedEventHandler PropertyChanged)
* subscribe to the CollectionChanged event (Classnames.CollectionChanged += ...)
* fire the event for listeners

Best,

/jhd


ViewModel update per above... ValueConverter now called on all changes to the Property/Collection

public class ViewModel : INotifyPropertyChanged
{
    private readonly ObservableCollection<string> _classnames = new ObservableCollection<string>();

    public ViewModel()
    {
        Classnames.CollectionChanged += Classnames_CollectionChanged;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void Classnames_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        NotifyPropertyChanged("Classnames");
    }

    private void NotifyPropertyChanged(string info)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            foreach (PropertyChangedEventHandler d in handler.GetInvocationList())
            {
                    d(this, new PropertyChangedEventArgs(info));
            }
        }
    }

    public ObservableCollection<string> Classnames
    {
        get { return _classnames; }
    }
}

The XAML binding...

<UserControl.Resources>
    <local:ViewModel x:Key="TheViewModel"/>
    <local:TabConverter x:Key="TabConverter" />
</UserControl.Resources>

<StackPanel DataContext="{StaticResource TheViewModel}">
    <ListBox ItemsSource="{Binding Classnames}" />
    <controls:TabControl x:Name="TheTabControl" 
        ItemsSource="{Binding Classnames, Converter={StaticResource TabConverter}, ConverterParameter={StaticResource TheViewModel}}"/>
    <Button Click="Button_Click" Content="Change Classnames" />
</StackPanel>

The ValueConverter (basically unchanged

    public class TabConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var source = value as ObservableCollection<string>;
            if (source == null)
                return null;

            //also sorted out the binding syntax to pass the ViewModel as a parameter
            var viewModel = parameter as ViewModel;
            if (viewModel == null)
                throw new ArgumentException("ConverterParameter must be ViewModel (e.g. ConverterParameter={StaticResource TheViewModel}");

            var tabItems = new List<TabItem>();
            foreach (string classname in source)
            {
                // real code dynamically loads controls by name
                var tabItem = new TabItem
                                  {
                                      Header = "Tab " + classname,
                                      Content = new Button {Content = "Content " + classname}
                                  };
                tabItems.Add(tabItem);
            }

            return tabItems;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
like image 79
John Dhom Avatar answered Oct 18 '22 05:10

John Dhom


I realize this is a slightly old question at this point, but I don't know that anyone has explained why you need to do the INotifyPropertyChanged on the bound property on your view model.

The ItemsControl itself needs to be bound to an ObservableCollection for the collection change events to cause the ItemsControl to re-evaluate. Your converter is returning a distinct List (or Observable) collection each time it is called rather than holding on to a single ObservableCollection and adding items to it. Therefore, these collections never have any of the collection changed events raised on them... they're always new, each time the binding is re-done.

Raising PropertyChanged forces the binding to be re-evaluated and re-runs your converter, returning a distinct collection and reflecting your changes.

I feel a better approach may be to do the conversion in your ViewModel rather than in a Converter. Expose an ObservableCollection of TabItem that you bind directly to and that you modify in place. The TabControl should then see changes made directly to your collection without the need to raise PropertyChanged and re-evaluate the entire binding.

[Edit - Added my approach] ViewModel: public class TabSampleViewModel { private ObservableCollection _tabItems = new ObservableCollection();

    public TabSampleViewModel()
    {
        AddTabItem("Alpba");
        AddTabItem("Beta");
    }

    public ObservableCollection<TabItem> TabItems
    {
        get
        {
            return _tabItems;
        }
    }

    public void AddTabItem( string newTabItemName )
    {
        TabItem newTabItem = new TabItem();

        newTabItem.Header = newTabItemName;
        newTabItem.Content = newTabItemName;

        TabItems.Add( newTabItem );
    }
}

View: <controls:TabControl ItemsSource="{Binding TabItems}"/>

like image 36
Ben Von Handorf Avatar answered Oct 18 '22 05:10

Ben Von Handorf