I have a ComboBox with its Items property bound to a collection of objects. I also have SelectedItem property bound to the entire collection, with a ValueConverter designed to examine the elements in the collection and return the 1 item to be selected. This part works.
What doesn't work is when the user makes a selection change on the ComboBox, the ConvertBack(...) method of the ValueConverter is not being called. I need ConvertBack(...) called because I need to take the user's selection, re-examine the collection, and edit the old selected item and newly selected item appropriately.
I know this approach is a awkward, but it's the way it is. Here's the relevant code:
ComboBox:
<ComboBox ItemsSource="{Binding}" SelectedItem="{Binding Path=., Converter={StaticResource ResourceKey=DataInputAssetChoiceSelectedItemConverter}}" />
ValueConverter:
public class DataInputAssetChoiceSelectedItemConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null)
{
foreach (CustomObject Choice in (Collection<CustomObject>)value)
{
if (Choice.IsSelected)
{
return Choice;
}
}
return ((Collection<CustomObject>)value).First();
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{ //breakpoint...execution never gets here!
return null;
}
}
So why doesn't ConvertBack(...) ever get called? Is it just something I'm misunderstanding about ComboBox? I've tried this approach using SelectedItem, SelectedValue, SelectedIndex, and have tried messing with UpdateSourceTrigger, various binding modes, DataTriggers, and can never seem to get ConvertBack(...) to be called. Is using the SelectionChanged event the only option? If so, why?
You are not binding to a property, so the Binding can't set anything. You are binding directly to the DataContext object, and the Binding won't update that.
If you had {Binding Path=SomeProperty, Converter=...} then the ConvertBack would be called. As it stands though, it won't be called.
You're right, it is awkward, but only because you're trying to add some management to the collection in a value converter instead of on the collection itself. I think it would help if your collection was more aware that its items have an IsSelected property:
public CustomCollection : Collection<CustomObject> {
CustomObject _current;
public CustomObject CurrentSelection {
get { return _current; }
set {
if (_current == value)
return;
if (_current != null)
_current.IsSelected = false;
if (value != null)
value.IsSelected = true;
_current = value;
}
}
}
Just add a little extra to make sure that _current is at least the first element in the collection.
<ComboBox ItemsSource="{Binding}" SelectedItem="{Binding CurrentSelection}">
Now you shouldn't need the converter anymore. There are some considerations missing, however. You may want to use ObservableCollection<T> instead and raise the PropertyChanged event when CurrentSelection is changed, so that if anything else is bound to that property or it changed in code, all bindings will update appropriately.
Edit: Wrapping the model
One easy way to wrap the collection instead of making a custom collection like above:
public class CollectionWrapper : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged = (o,e)=>{};
// never have to check for null
public CollectionWrapper(Collection<CustomObject> collection) {
Items = collection;
}
// unlikely to change, so let's prevent it for now
public Collection<CustomObject> Items {
get;
private set;
}
CustomObject _current;
public CustomObject CurrentSelection {
get { return _current; }
set {
if (_current == value)
return;
if (_current != null)
_current.IsSelected = false;
if (value != null)
value.IsSelected = true;
_current = value;
PropertyChanged(this, new PropertyChangedEventArgs("CurrentSelection"));
}
}
}
Then this object becomes your data context and the ComboBox bindings change to this:
<ComboBox ItemsSource="{Binding Items}" SelectedItem="{Binding CurrentSelection}">
Related
I have several Tiles (TileLayoutControl Class) in my xaml (only shown 2 in this example) whose Visibility are binded to Boolean Properties and converted through BooleanToVisibilityConverter.
This works just fine. My question is
Can I bind the visibility to the Command instead so that I can remove the need of those several Boolean Properties?
Something like binding the Visibility to Command.CanExecute
If Yes, How can I achieve that? Any help will be really appreciated! Thanks.
<dxlc:Tile Command="{Binding Tile1Command}"
Visibility="{Binding Path=IsTile1Visible , Converter={StaticResource BooleanToVisibilityConverter}}"/>
<dxlc:Tile Command="{Binding Tile2Command}"
Visibility="{Binding Path=IsTile2Visible , Converter={StaticResource BooleanToVisibilityConverter}}"/>
ViewModel
private bool _isTile1Visible;
public bool IsTile1Visible
{
get { return _isTile1Visible; }
set { this.RaiseAndSetIfChanged(ref _isTile1Visible, value); }
}
public ReactiveCommand Tile1Command { get; private set; }
Tile1Command = new ReactiveCommand();
Tile1Command.Subscribe(p => PerformTile1Operation());
Yes, just use RxUI bindings:
<dxlc:Tile x:Name="Tile1" />
Then in your View constructor (make sure to implement IViewFor<Tile1ViewModel> to get this extension):
this.BindCommand(ViewModel, x => x.Tile1Command);
this.WhenAnyObservable(x => x.ViewModel.Tile1Command.CanExecuteObservable)
.BindTo(this, x => x.Tile1.Visibility);
You could also solve this in the ViewModel level, though that's not what I would do - in the ViewModel ctor:
Tile1Command = new ReactiveCommand(/* ... */);
Tile1Command
.Select(x => x ? Visibility.Visible : Visibility.Collapsed)
.ToProperty(this, x => x.Tile1Visibility, out tile1Visibility);
ReactiveCommand is an ICommand implementation that is simultaneously a RelayCommand implementation...
Assume that the ReactiveCommand has been declared like this...
public ReactiveCommand FileCommand { get; private set; }
...and has been instantiated in a View Model like this...
SomeText = "";
FileCommand = new ReactiveCommand(this.WhenAny(vm => vm.SomeText, s => !string.IsNullOrWhiteSpace(s.Value)));
FileCommand.Subscribe(param => MessageBox.Show("Processing"));
... which means if the property SomeText is empty, then the command cannot be executed, otherwise the command can be executed. And if the command is executed, a message box will get displayed.
If your objective is to simply eliminate the boolean IsTile1Visible, you can make a Xaml declaration like this...
<Button Content="File"
Command="{Binding FileCommand}"
Visibility="{Binding FileCommand, Converter={genericMvvm1:CommandToVisibilityConverter}}" />
where the visibility is bound to the same command and uses a value converter...
and the value converter looks like this...
public class CommandToVisibilityConverter : MarkupExtension, IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
try
{
ICommand iCommand = value as ICommand;
if (iCommand != null)
{
if (iCommand.CanExecute(parameter))
{
return Visibility.Visible;
}
return Visibility.Collapsed;
}
}
catch
{
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
The value converter simply dereferences the command into a basic ICommand and converts it into a visibility. Note that since this converter inherits from Markup Extension, there's no need to declare it as a static resource in the Xaml's object graph.
NOTE: the same functionality can be achieved by using 'code-behind' available in ReactiveUI, but the Xaml/ValueConverter appeals to developers who do not want their View Models to deal explicitly with the 'Visibility' property.
You could potentially do that, but it would require subclassing the command so that it also implements INotifyPropertyChanged, and the underlying condition would need to raise PropertyChange for the CanExecute property whenever it changes.
It won't work without that, as ICommand doesn't implement INotifyPropertyChanged - it uses CanExecuteChanged instead.
Note that you could simplify the property, however, by just handling it yourself in the constructor:
// In constructor:
Tile1Command = new ReactiveCommand();
Tile1Command.Subscribe(p => PerformTile1Operation());
IReactiveObject self = this as IReactiveObject;
Tile1Command.CanExecuteChanged += (o,e) => self.RaisePropertyChanged(new PropertyChangedEventArgs("IsTile1Visible"));
Then your property becomes:
// Use command directly here...
public bool IsTile1Visible
{
get { return Tile1Command.CanExecute; }
}
I have a ObservableCollection which I need to bind to 2 labels, first to show count of items in the collection and second to show the sum of values.
First label is bound to collections count property and second label is bound directly to ObservableCollection with a convertor to calculate total of all items
XAML looks something like this
<Grid>
<ListBox Name="itemList" ItemsSource="{Binding DataList}"/>
<Label Name="lblcount" Content="{Binding DataList.Count}" />
<Label Name="lblTotal" Content="{Binding DataList, Converter={StaticResource calculateTotalConvertor}" />
</Grid>
My VM has a collection like this
ObservableCollection<int> data = new ObservableCollection<int>();
public ObservableCollection<int> DataList
{
get { return data; }
set { data = value; }
}
My convertor code is
public class CalculateTotalConvertor : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
ObservableCollection<int> collection = value as ObservableCollection<int>;
return collection.Sum();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Issue is on adding new items in DataList, ListView and label showing count of items gets updated but "lblTotal" doesnt get updated with total count.
Basically how to force your binding to be evaluated on ObservableCollection changes ? How does it work directly for ListView or DataGrid but not for label ?
I know this problem can be solved by creating a property in VM to show total and raise property change when collection gets updated but is there is any better solution than that ?
Of-course this is simplified form of my actual problem, I dont have access to the ViewModel and the collection, its a third party control. I am creating a wrapper user control and have a relative binding with the view to its inner collection.
The other answers correctly explain why it is not updating. To force it to update you can change your converter to an IMultiValueConverter:
public class CalculateTotalConvertor : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
ObservableCollection<int> collection = values.FirstOrDefault() as ObservableCollection<int>;
return collection.Sum();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Then change your binding to a MultiBinding which also pulls in the Count:
<Label Name="lblTotal">
<Label.Content>
<MultiBinding Converter="{StaticResource calculateTotalConvertor}">
<Binding Path="DataList"/>
<Binding Path="DataList.Count"/>
</MultiBinding>
</Label.Content>
</Label>
Now the second binding will notify that the binding needs to update when items are added or removed, but you can just ignore the count value and not use it.
Its not updating because its bound to DataList and DataList has not changed, The count label updates because its bound to DataList.Count which is updated when an item is added to the list.
The only way I can think of to update the Sum label is to notify the UI that the DataList has changed, but this will cause the ListBox to rebind the list and it will performace will be a lot more expensive than just having a property on your model update the Sum.
So I think the best option would be to use a property on your model to caculate the sum using the ObservableCollections CollectionChangedEvent or in the logic that adds items to the list
It works for ListView and DataGrid, because these are ItemsControls that listen to the ObservableCollection's CollectionChangedEvent, which is raised when the collection itself is changed by adding or removing items.
The Label on the other hand is a ContentControl that only listens to the PropertyChangedEvent. Since your DataList is the same ObservableCollection after the insertion as it was before, no events are raised.
Just saw your edit:
If you are creating a wrapping control, give the 3rd party control a name and hook up to its inner collection's CollectionChangedEvent from your control's code behind. That way you can still push update notifications to your wrapping view.
Go with the extra property, it will save you some code on the converter. From the code behind:
public partial class MainWindow : Window, INotifyPropertyChanged
{
ObservableCollection<int> _list = new ObservableCollection<int>();
int _sum = 0;
Random rnd = new Random();
public MainWindow()
{
DataList = new ObservableCollection<int>();
DataList.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(DataList_CollectionChanged);
DataContext = this;
InitializeComponent();
}
void DataList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (object number in e.NewItems)
_sum += (int)number;
break;
case NotifyCollectionChangedAction.Remove:
foreach (object number in e.OldItems)
_sum -= (int)number;
break;
}
OnNotifyPropertyChanged("Sum");
}
public int Sum { get { return _sum; } }
public ObservableCollection<int> DataList { get; set; }
private void Add_Btn_Click(object sender, RoutedEventArgs e)
{
DataList.Add(rnd.Next(0, 256));
}
private void Remove_Btn_Click(object sender, RoutedEventArgs e)
{
if (DataList.Count == 0)
return;
DataList.RemoveAt(DataList.Count - 1);
}
public event PropertyChangedEventHandler PropertyChanged;
void OnNotifyPropertyChanged(string property)
{
if (PropertyChanged == null)
return;
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
My ViewModel class has a child property of type 'Messages' that has an indexed property, like:
public class ViewModel
{
// ...
public Messages Messages
{
get
{
if (_messages == null)
{
LoadMessagesAsync();
_messages = new Messages();
}
return _messages;
}
set
{
_messages = values;
PropertyChanged(new PropertyChangedArgs("Messages");
}
}
// ...
private void LoadMessagesAsync()
{
// Do the service call
Messages = theResult;
}
}
public class Messages
{
// ...
public String this[String name]
{
get { return _innerDictionary[name]; }
}
// ...
}
I don't think I need to fill in the rest of the gaps as it is all straight-forward.
The problem I am having is that the binding is not updating when I set the Messages property to a new object. Here is how I am referencing the property in XAML (with ViewModel as the DataContext):
<TextBlock Text="{Binding Messages[HelloWorld]}" />
It was my assumption that the binding would update when the PropertyChanged event was raised for the "Messages" property.
I've read elsewhere that my Messages class should raise a PropertyChanged event with either an empty string (""), "Item[]" or "Item["+name+"]" for the property name. However, since I am completely replacing the Messages object, this won't work as I never actually change the contents.
How do I make this work?
UPDATE
So I've done some digging into the behavior and into the BCL source code to see what's expected as a way to figure out how to make my code work. What I've learned is two-fold:
First, Silverlight data-binding is actually looking at the return object from the Messages property as the source of the binding. So raising PropertyChanged from ViewModel (sender is ViewModel) is not handled by the binding. I actually have to raise the event from the Messages class.
This is no different than using the following: Text={Binding Messages.HelloWorld}"
The reason that Myles' code work is that 'Data' returns 'this' so the binding is fooled into treating the parent class as the binding source.
That said, even if I make it so my child object raises the event, it still won't work. This is because the binding uses the System.Windows.IndexerListener as the binding target. In the SourcePropertyChanged method, the listener checks if the property name is "Item[]" but takes no action. The next statement delegates to the PropertyListener which checks the property name and only handles the event if it is equal to "Item[HelloWorld]".
So, unless I explicitly raise the event for each possible value within my collection, the UI will never update. This is disappointing because other articles and posts indicate that "Item[]" should work but looking at the source proves otherwise.
Nevertheless, I still hold out hope that there is a way to accomplish my goals.
OK the underlying problem here is that the Binding does not have a Path specified, therefore, the binding framework does not know which property name to look out for when handling PropertyChanged events. So I have fabricated a Path for the binding in order for change notification to work.
I have wrote the following code that proves the indexer binding is refreshed when the actual underlying dictionary changes:
ViewModel
public class BindingTestViewModel : AppViewModelBase, IBindingTestViewModel
{
private Dictionary<string, object> _data = new Dictionary<string, object>();
public BindingTestViewModel()
{
_data.Add("test","1");
_data.Add("test2", "21");
}
public object this[string index]
{
get
{
return _data[index];
}
set
{
_data[index] = value;
NotifyOfPropertyChange(() => Data);
}
}
public object Data
{
get
{
return this;
}
}
public void Refresh()
{
_data = new Dictionary<string, object>
{
{"test", "2"}, {"test2", "22"}
};
NotifyOfPropertyChange(() => Data);
}
}
My XAML:
<navigation:Page.Resources>
<Converters:IndexConverter x:Name="IndexConverter"></Converters:IndexConverter>
</navigation:Page.Resources>
<Grid x:Name="LayoutRoot">
<TextBox Height="23"
HorizontalAlignment="Left"
Margin="288,206,0,0"
Name="textBox1"
Text="{Binding Path=Data,Converter={StaticResource IndexConverter},ConverterParameter=test}"
VerticalAlignment="Top" Width="120" />
<Button x:Name="ReloadDict" Click="ReloadDict_Click" Width="50" Height="30" Content="Refresh" VerticalAlignment="Top"></Button>
</Grid>
The converter:
public class IndexConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
var vm = value as BindingTestViewModel;
var index = parameter as string;
return vm[index];
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
I don't necessarily like answering my own questions but I have found a solution to my problem. I've been able to keep my original flow and address the idiosyncrocies of SL4 data-binding. And the code seems a bit cleaner, too.
What it boils down to is that I don't replace the child object anymore. That seems to be the key. Instead, I create a single instance and let that instance manage changing the internal list of items as needed. The child object notifies the parent when it has changed so the parent can raise the PropertyChanged event. The following is a brief example how I've gotten it to work:
public class ViewModel
{
// ...
public Messages Messages
{
get
{
if (_messages == null)
{
lock (_messagesLock)
{
if (_messages == null)
{
_messages = new Messages();
_messages.ListChanged += (s, e) =>
{
NotifyPropertyChanged("Messages");
};
}
}
}
return _messages;
}
}
}
public class Messages
{
// ...
public String this[String name]
{
get
{
if (_innerDictionary == null)
{
_innerDictionary = new Dictionary<String, String>();
LoadMessagesAsync();
}
return _innerDictionary[name];
}
}
// ...
private void LoadMessagesAsync()
{
// Do the service call
_innerDictionary = theResult;
NotifyListChanged();
}
// ...
public event EventHandler ListChanged;
}
For brevity, I've left out the obvious parts.
I am trying to display filenames in a listbox, retrieved from a particular directory. They are stored in an ObservableCollection of FileInfo objects:
public ObservableCollection<FileInfo> ProjectFiles
{
get
{
if (SelectedFolder == null) return null;
DirectoryInfo d= new DirectoryInfo(SelectedFolder);
if (!d.Exists) return null;
return new ObservableCollection<FileInfo>(d.EnumerateFiles("*.xsi"));
}
}
I have implemented a filter on the listbox, called when text is entered or changed in a textbox "FilesFilterBy":
private void FilterFiles_TextChanged(object sender, TextChangedEventArgs e)
{
ICollectionView view = CollectionViewSource.GetDefaultView(ProjectFiles);
view.Filter = new Predicate<object>(IsTextInFilename);
}
public bool IsTextInFilename(object item)
{
string Filename = Path.GetFileNameWithoutExtension((item as FileInfo).Name);
return (Filename.ToLower().Contains(FilesFilterBy.Text.ToLower()));
}
At the same time, I want to display only the names of the files, without path or extension. To this end I have implemented a converter:
public class RemoveExtensionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return Path.GetFileNameWithoutExtension(value as string);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return new NotImplementedException();
}
}
Here is how the listbox is implemented in XAML:
<Window.Resources>
<ctr:RemoveExtensionConverter x:Key="JustFileName" />
</Window.Resources>
<ListBox ItemsSource="{Binding ProjectFiles}" >
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding FullName, Converter={StaticResource JustFileName}}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Currently the converter works - only the file names are listed, but the filter no longer has any effect. When I enter text in the FileFilterBy textbox the TextChanged event is fired but the listbox stays the same. Also, the converter is not called at that point.
What am I doing wrong?
ProjectFiles returns a new collection every time. Your FilterFiles_TextChanged handler is calling ProjectFiles to create a new collection, setting a filter on that new collection, and then throwing it away. The collection bound to the ListBox is not affected. You need to change ProjectFiles to keep the same collection object. Maybe something like this:
private ObservableCollection<FileInfo> _projectFiles;
public ObservableCollection<FileInfo> ProjectFiles
{
get
{
if (_projectFiles == null)
{
if (SelectedFolder == null) return null;
DirectoryInfo d = new DirectoryInfo(SelectedFolder);
if (!d.Exists) return null;
_projectFiles = new ObservableCollection<FileInfo>(
d.EnumerateFiles("*.xsi"));
}
return _projectFiles;
}
}
The Converter shouldn't affect the filter at all.
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();
}
}
}
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();
}
}
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}"/>
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.
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.