I was just looking into the difference between BindingList and ObservableCollection following this question: Why NOT BindingList in WPF
As part of this, I tested binding the ItemsSource of an ItemsControl to various types, including List, Collection, ObservableCollection and BindingList.
What surprised me is that the interface updated when either the ObservableCollection or the BindingList were modified, but not when the others were. So what is WPF listening to that causes that update? It can't be the INotifyCollectionChanged event, as I previously thought, because BindingList does not implement that. Bemused.
Binding list looks like this:
public class BindingList<T> : Collection<T>, IBindingList, IList, ICollection, IEnumerable, ICancelAddNew, IRaiseItemChangedEvents
{
IRaiseItemChangedEvents indicates that the object class converts property change events to ListChanged events
.
BindingList itself has the ListChanged event which is what WPF must be listening to.
If fact it looks like IRaiseItemChangedEvents is ignored, but there's a BindingListCollectionView which contains
// subscribe to change notifications
private void SubscribeToChanges ()
{
if (InternalList.SupportsChangeNotification)
{
InternalList.ListChanged += new ListChangedEventHandler(OnListChanged);
}
}
and a constructor like
/// <summary>
/// Constructor
/// </summary>
/// <param name="list">Underlying IBindingList</param>
public BindingListCollectionView(IBindingList list)
: base(list)
{
InternalList = list;
I recommend you get hold of DotPeek and see for yourself.
Related
I have a 2-levels hierarchy in my model composed of constellations and entities, hosted in a root object, and I want to show them in a TreeView.
Root object
L Constellations
L Entities
I have my RootObjectViewModel exposing my root object. In that, I fully agree with Bryan Lagunas in MVVM best practices, that the model object should be exposed by the view model instead of doing facade. Excellent video by the way, really worth the time.
I read everywhere that the TreeView.ItemsSource should be mapped to a collection of viewmodels, that is, for my collection of Constellation, my RootObjectViewModel should provide a collection of ConstellationViewModel.
My concern is that if my collections of constellations, and entities within, are live, that is, if some items are added, changed (their order) or removed, I have to manually reflect those changes in my ViewModels' collections.
I would find it more elegant to map the ItemsSource to, say, the collection of Constellation itself, so that any change in the model is reflected without duplication, and then have some converter or so to map the TreeViewItems.DataContext to a ConstellationViewModel based on the constellation model.
If this is not clear enough, I can clarify this with some sample code.
Did anyone face the same question and/or try to implement this ?
Thanks you in advance for your help.
Cedric
It depends. If your model has exactly the properties the view needs, and the view can directly alter them when the user clicks around, it's fine to expose the model.
But if your model is, for example, read only and require calls to a service to apply changes, you have to wrap it in a view model to provide the view with writeable properties.
Got it working !
It is not possible out-of-the-box, and here's why:
It is possible to use model collections as items source, and to use a converter to get the appropriate view model it the components inside the TreeViewItem. But there isn't any way to interfere with the creation of TreeViewItem to apply the converter to its DataContext. Which means that the TreeViewItem's properties can't be binded to the ViewModel.
In other words :
if you want to stick with the standard behavior of the TreeView and don't have to deal with the TreeViewItems properties, if either your collections don't change or can implement ICollectionChanged, and if your models don't change or can implement IPropertyChanged, it is fine to go with the model's collections.
If any of these conditions is broken, then you will have to go with building ViewModel's collections and sync them with model's collection.
Now, I implemented a collection type named ConvertingCollection<Tin, Tout> that uses an original collection as an input and syncs its own contents with this input. This is just a basic class with many many ways of improvement, but it works. All you have to do is use this collection as a VM property, set the original collection and converter, and bind the ItemsSource to this collection.
Here's the full code:
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Globalization;
using System.Windows.Data;
namespace TreeViewPOC
{
class ConvertingCollection<Tin, Tout> : ObservableCollection<Tout>
{
private IValueConverter _converter;
private bool _isObservableCollection;
private IEnumerable<Tin> _originalCollection;
private Dictionary<Tin, Tout> _mapping = new Dictionary<Tin, Tout>();
public ConvertingCollection(IValueConverter converter)
{
// save parameters
_converter = converter;
}
public ConvertingCollection(IEnumerable<Tin> originalCollection, IValueConverter converter)
{
// save parameters
_converter = converter;
OriginalCollection = originalCollection;
}
#region Properties
public IEnumerable<Tin> OriginalCollection
{
get
{
return _originalCollection;
}
set
{
if (!value.Equals(_originalCollection))
{
// manage older collection
if (_originalCollection != null && _isObservableCollection)
{
(_originalCollection as ObservableCollection<Tin>).CollectionChanged -= originalCollection_CollectionChanged;
this.Clear();
}
_originalCollection = value;
// setup original collection information.
_isObservableCollection = _originalCollection is INotifyCollectionChanged;
if (_originalCollection != null && _isObservableCollection)
{
(_originalCollection as INotifyCollectionChanged).CollectionChanged += originalCollection_CollectionChanged;
foreach (Tin item in _originalCollection)
{
AddConverted(item);
}
}
}
}
}
#endregion
/// <summary>
/// Indicates the time in milliseconds between two refreshes.
/// </summary>
/// <notes>
/// When the original collection isn't observable, it must be explored to reflect changes in the converted collection.
/// </notes>
// TODO
//public int RefreshRate { get; set; } = 1000;
/// <summary>
/// Flushes the collection.
/// </summary>
public new void Clear()
{
_mapping.Clear();
base.Clear();
}
#region Events management
private void originalCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (Tin item in e.NewItems)
{
AddConverted(item);
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (Tin item in e.OldItems)
{
RemoveConverted(item);
}
break;
}
}
#endregion
#region Helpers
/// <summary>
/// Converts an item and adds it to the collection.
/// </summary>
/// <param name="item">The original item.</param>
private void AddConverted(Tin item)
{
Tout converted = (Tout) _converter.Convert(item, typeof(Tout), null, CultureInfo.CurrentCulture);
_mapping.Add(item, converted);
this.Add(converted);
}
/// <summary>
/// Removes a converted itemfrom the collection based on its original value.
/// </summary>
/// <param name="item">The original item.</param>
private void RemoveConverted(Tin item)
{
this.Remove(_mapping[item]);
_mapping.Remove(item);
}
#endregion
}
}
I created a project on Github named MVVMTreeViewPOC to POC the idea. It is working fine, and it is a good tool to show the limitations.
Cedric.
I am using a custom control derived from a listbox, but with added features. One of the key features is the addition of a bindable SelectedItems property on the control, so the view model can keep track of the multiple selections made in the control. The binding does work - when you select items in the control, the view model's property is updated. However, I would like to add INotifyDataErrorInfo validation to the view model, so I implemented the interface and added a call to my validation method in the set block of the data-bound property in the viewmodel. For some reason that set block is never being called, even though I am updating the control in the view, and am verifying that the view model's property value is actually being changed correctly to match the control.
I know that when I use binding with standard WPF controls, such as a TextBox, the set block of the source (view model) property is called when the target (view) property changes. Is there a reason it wouldn't be called here?
The custom control I am using is found here. This is my property on the viewmodel (I have the console output there just to ensure the code isn't being called):
private ObservableCollection<Car> _testListSelections;
public ObservableCollection<Car> testListSelections
{
get
{
return _testListSelections;
}
set
{
Console.WriteLine("Value changed.");
_testListSelections = value;
OnPropertyChanged("testListSelections");
Validate();
}
}
This is my XAML (note that I didn't need to use Mode=TwoWay here as I am using an ObservableCollection, but I did try specifying Mode=TwoWay and the set block still didn't get hit):
<src:MultiComboBox SelectionMode="Multiple"
VerticalAlignment="Center"
ItemsSource="{Binding testList}"
SelectedItems="{Binding testListSelections, ValidatesOnNotifyDataErrors=True}"/>
This is the SelectedItems property on the custom control (the author overrode the base read-only SelectedItems property in order to allow binding):
/// <summary>
/// The SelectedItems dependency property. Access to the values of the items that are
/// selected in the selectedItems box. If SelectionMode is Single, this property returns an array
/// of length one.
/// </summary>
public static new readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(IList), typeof(BindableListBox),
new FrameworkPropertyMetadata(
(d, e) =>
{
// When the property changes, update the selected values in the selectedItems box.
(d as BindableListBox).SetSelectedItemsNew(e.NewValue as IList);
}));
/// <summary>
/// Get or set the selected items.
/// </summary>
public new IList SelectedItems
{
get
{
return GetValue(SelectedItemsProperty) as IList;
}
set { SetValue(SelectedItemsProperty, value); }
}
You should perform the validation in the OnCollectionChanged event of the list.
The SelectedItems list should be set only once, and then changes are made to the same list.
You can then check if the operation is Add, Remove or Reset, and perform validation accordingly.
I have a simple Datagrid binded to an ObservableCollection from the ViewModel. This ObservableCollection is composed by a Custom Type, say ObservableCollection.
The ComplexType only have 2 properties, and only one is editable on the screen. The other one is a bool type that depends on the first.
When I edit the first property, it gets reflected to the ComplexType and it also change the second property. But the second property is not changed on the screen.
How can I update the second property on the screen?
Try this:
public class ComplexType:INotifyPropertyChanged
{
private object someProperty1;
public object SomeProperty1
{
get{return someProperty1;}
set
{
someProperty1=value;
SomeProperty2=somefunc(someProperty1);
If(PropertyChanged!=null){PropertyChanged(this, new PropertyChangedEventArgs(SomeProperty1));}
}
}
private object someProperty2;
public object SomeProperty2
{
get{return someProperty2;}
set
{
someProperty2=value;
If(PropertyChanged!=null){PropertyChanged(this, new PropertyChangedEventArgs(SomeProperty2));}
}
public event PropertyChangedEventHandler PropertyChanged;
}
An observable collection provides notification only when items are added, removed, or the whole collection is refreshed. You need to make sure that each property either raises the PropertyChanged event or is a dependency property if you want your UI to refresh when it changes.
I have a ComboBox that has its ItemSource and SelectedItem properties bound to a view model. And I have the following block of code that is the callback for a data query against a DomainContext:
/// <summary>
/// Stores (readonly) - Stores available for ship to store.
/// </summary>
public ObservableCollection<StoreEntity> Stores
{
get { return _stores; }
private set { _stores = value; RaisePropertyChanged("Stores"); }
}
/// <summary>
/// SelectedStore - Currently selected store.
/// </summary>
public StoreEntity SelectedStore
{
get { return _selectedStore; }
set { _selectedStore = value; RaisePropertyChanged("SelectedStore"); }
}
/// <summary>
/// When stores are completely loaded.
/// </summary>
/// <param name="a_loadOperations"></param>
protected void OnStoresLoaded(LoadOperation<StoreEntity> a_loadOperations)
{
Stores.AddRange(a_loadOperations.Entities);
SelectedStore = a_loadOperations.Entities.FirstOrDefault();
}
In this example, Stores is a ObservableCollection<StoreEntity> (AddRange is an extention method) and is bound to ItemSource, and SelectedStore is a StoreEntity and is bound to SelectedItem.
The problem here is that the ComboBox is not changing its selection to reflect the change in SelectedItem.
Edits:
I've even tried the following, though I think that a_loadOperation.Entities is already a realized set:
/// <summary>
/// When stores are completely loaded.
/// </summary>
/// <param name="a_loadOperations"></param>
protected void OnStoresLoaded(LoadOperation<StoreEntity> a_loadOperations)
{
var entities = a_loadOperations.Entities.ToArray();
Stores.AddRange(entities);
SelectedStore = entities.First();
}
Thanks
If you are trying to get a change to your viewmodel (specifically the SelectedStore property) to be reflected in your combo box, you could:
Confirm the binding worked. Check it was set up properly in the XAML, and check the Output window to see if there is a message saying the Binding failed
Confirm your DataContext is set properly (it probably is since you are getting the combo box `ItemsSource` from `Stores`
Subscribe to your PropertyChanged event and confirm that it is being raised when the property changes
If that doesn't work, it may be a bug with the ComboBox. I have seen cases where the order of specifying the properties in XAML makes the difference (ex: you should set ItemsSource first and SelectedItem second). I have also seen a binding fail until I added Mode=TwoWay (even though in your example you are trying to get the binding to update from your view model to your UI). Try confirming that your ComboBox XAML is like this:
<ComboBox ItemsSource="{Binding Stores}" SelectedItem="{Binding SelectedStore, Mode=TwoWay}" />
Order shouldn't matter since XAML is declarative, but I have personally seen it matter with ComboBoxes in Silverlight.
I had SelectedItem bound to Stores instead of SelectedStore. Ooops!
i have a mvvm app that the main window is a tab control.
i use the itemssource to bind items to the combo box,
everything works fine until i go to another tab and for some reason the selected item of the combo box getting the null value, any ideas ?
the binding is twoway updatesource onpropertychanged and the property is type of observablecollection
I had same problem before. And the solution is that make sure Itemsource attribute of comboBox in XAML has not been declared before SelectedValue attribute. It should work then.
It is just necessary to work on a ObservableCollection and to load only as AddRange (not use 'Add')
When we load the data as:
foreach (object item in items)
{
MyList.Add(item); // Where MyList is a ObservableCollection
}
Then after adding of the first element the ObservableCollection call OnCollectionChanged.
Then the ComboBox will try to select the SelectedValue and return 'null' when can't find this element.
It can solve a problem:
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
namespace System.Collections.ObjectModel
{
/// <summary>
/// Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed.
/// </summary>
/// <typeparam name="T"></typeparam>
public class ObservableCollectionEx<T> : ObservableCollection<T>
{
/// <summary>
/// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class.
/// </summary>
public ObservableCollectionEx()
: base() { }
/// <summary>
/// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class that contains elements copied from the specified collection.
/// </summary>
/// <param name="collection">collection: The collection from which the elements are copied.</param>
/// <exception cref="System.ArgumentNullException">The collection parameter cannot be null.</exception>
public ObservableCollectionEx(IEnumerable<T> collection)
: base(collection) { }
/// <summary>
/// Adds the elements of the specified collection to the end of the ObservableCollection(Of T).
/// </summary>
public void AddRange(IEnumerable<T> collection)
{
//
// Add the items directly to the inner collection
//
foreach (var data in collection)
{
this.Items.Add(data);
}
//
// Now raise the changed events
//
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
}
I have an mvvm app with almost exactly the same scenario. The main window has a tab control. There is a tab containing a combobox. The combobox itemsource is bound to an IList (in the view model) and the Selected value is bound to a property in the view model implementing INotifyPropertyChanged.
<ComboBox ItemsSource="{Binding AllowedJudges}"
SelectedValue="{Binding SelectedJudge, UpdateSourceTrigger=PropertyChanged}" >
When selecting another tab, the view model's property bound to the SelectedValue mysteriously gets set to null. I'm able to handle it by not allowing the SelectedValue-bound property to be set to null:
public Judge SelectedJudge
{
get { return selectedJudge; }
set
{
if(selectedJudge==value || value==null) return;
selectedJudge = value;
OnPropertyChanged("SelectedJudge");
updateViewData();
}
}
However, it's not clear to me why a tab pane becoming invisible implies a value in a combobox there becomes deselected....
If for some Reason the BindingSource of the ItemsSource does no longer contain the SeletedItem (because it's re-initialized or whatever), then the SelectedItem can be reset to default, i.e. null.
From your example I can't tell why this should happen, but that maybe because you just missed adding some more environmental code.
It`s may be problem of your viewmodel ierarchy.
For example, if both ends of your Binding are dependency properties and ownertype property is not tied to a particular class (for example, set the parent class), this dependency property will be used by all the inheritors together. Bad design