views:

3446

answers:

4

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();
        }
    }
}
A: 

Expose

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

If you debug the valueconverter you'll see it's not being called as often as you think it is.

Graeme Bradbury
Are you saying the ValueConverter isn't supposed to be called when the bound source changes? This may be correct but is counterintuitive to me. Given the ViewModel is currently in the Data project/namespace I'd prefer not to reference S.W.C to get TabItem, but it's a better workaround than I have now. Thanks. /jhd
John Dhom
Hi Graeme, the ValueConverter wasn't being called (after initial binding) because I didn't implement INotifyPropertyChanged on the ViewModel. See my answer below.
John Dhom
A: 

The problem could be that your ValueConverter returns a List<TabItem> instead of an ObservableCollection<TabItem>. Try that one line change and see if it helps.

James Cadd
Good catch, alas changing it has no [obvious] effect. Isn't databinding subscribed to the source, Classnames in the case?
John Dhom
+2  A: 

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();
        }
    }
John Dhom
note: You don't want to send the PropertyChanged event on every CollectionChanged event in real code... you should raise one PropertyChanged event after you're done manipulation the source collection. /jhd
John Dhom
A: 

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}"/>

Ben Von Handorf