How to implement Master-Detail with Multi-Selection in WPF? - wpf

I plan to create a typical Master-Detail scenario, i.e. a collection of items displayed in a ListView via DataBinding to an ICollectionView, and details about the selected item in a separate group of controls (TextBoxes, NumUpDowns...).
No problem so far, actually I have already implemented a pretty similar scenario in an older project. However, it should be possible to select multiple items in the ListView and get the appropriate shared values displayed in the detail view. This means, if all selected items have the same value for a property, this value should be displayed in the detail view. If they do not share the same value, the corresponding control should provide some visual clue for the user indicating this, and no value should be displayed (or an "undefined" state in a CheckBox for example). Now, if the user edits the value, this change should be applied to all selected items.
Further requirements are:
MVVM compatibility (i.e. not too much code-behind)
Extendability (new properties/types can be added later on)
Does anyone have experience with such a scenario? Actually, I think this should be a very common scenario. However, I could not find any details on that topic anywhere.
Thanks!
gehho.
PS: In the older project mentioned above, I had a solution using a subclass of the ViewModel which handles the special case of multi-selection. It checked all selected items for equality and returned the appropriate values. However, this approach had some drawbacks and somehow seemed like a hack because (besides other smelly things) it was necessary to break the synchronization between the ListView and the detail view and handle it manually.

In your ViewModel, create a property that will bind to your ListView's SelectedItems.
Create another property that will represent the details object of your selected items.
The details section (in the XAML) is binding to this details property (in the ViewModel).
Every time selected items collection is modified (setter/CollectionChanged event), you need to update your details object as well (that will iterate through the relevant properties and decide if they're of the same value or not).
Once a property in the details object is modified, you need to iterate back on your selected items collection and make the relevant changes on them.
That's it.
Hope it helps

I am running into this same exact issue.
IsSynchronizedWithCurrentItem = "True" only keeps the CurrentItem in sync (the last item you selected without holding ctrl or shift).
Just as you probably have, I have searched the internet far and wide for an answer and never came up with one. My scenario has a 3 tier Master>Detail>Detail binding, where each tier is bound to it's own ListBox.
I've rigged up something that works for the time being.
For my Master>Detail>Detail tiers I created an individual CollectionViewSource for each tier and set that CollectionViewSource.Source to the appropriate entity object. On the MasterView bound ListBox's SelectionChanged Event I performed a filter on the MasterView.View to retrieve objects where the Master Primary Key = Detail Foreign Key.
It's sloppy, but if you have found a better way to get this done, I'd love to hear it.

I have used an approach similar to the one suggested by Captain. I have created one property in my ViewModel which represents multiple items (i.e. all selected items). In the property get- and set-accessors I have used the following methods to determine/set the shared values of/for all items. This approach does not use any reflection, but uses delegates in the form of lambda expressions which makes it pretty fast.
As an overview, this is my basic design:
public class MyMultiSelectionViewModel
{
private List<MyItemType> m_selectedItems = new List<MyItemType>();
public void UpdateSelectedItems(IList<MyItemType> selectedItems)
{
m_selectedItems = selectedItems;
this.OnPropertyChanged(() => this.MyProperty1);
this.OnPropertyChanged(() => this.MyProperty2);
// and so on for all relevant properties
}
// properties using SharedValueHelper (see example below)
}
The properties look like this:
public string Name
{
get
{
return SharedValueHelper.GetSharedValue<MyItemType, string>(m_selectedItems, (item) => item.Name, String.Empty);
}
set
{
SharedValueHelper.SetSharedValue<MyItemType, string>(m_selectedItems, (item, newValue) => item.Name = newValue, value);
this.OnPropertyChanged(() => this.Name);
}
}
And the code for the SharedValueHelper class looks like this:
/// <summary>
/// This static class provides some methods which can be used to
/// retrieve a <i>shared value</i> for a list of items. Shared value
/// means a value which represents a common property value for all
/// items. If all items have the same property value, this value is
/// the shared value. If they do not, a specified <i>non-shared value</i>
/// is used.
/// </summary>
public static class SharedValueHelper
{
#region Methods
#region GetSharedValue<TItem, TProperty>(IList<TItem> items, Func<TItem, TProperty> getPropertyDelegate, TProperty nonSharedValue)
/// <summary>
/// Gets a value for a certain property which represents a
/// <i>shared</i> value for all <typeparamref name="TItem"/>
/// instances in <paramref name="items"/>.<br/>
/// This means, if all wrapped <typeparamref name="TItem"/> instances
/// have the same value for the specific property, this value will
/// be returned. If the values differ, <paramref name="nonSharedValue"/>
/// will be returned.
/// </summary>
/// <typeparam name="TItem">The type of the items for which a shared
/// property value is requested.</typeparam>
/// <typeparam name="TProperty">The type of the property for which
/// a shared value is requested.</typeparam>
/// <param name="items">The collection of <typeparamref name="TItem"/>
/// instances for which a shared value is requested.</param>
/// <param name="getPropertyDelegate">A delegate which returns the
/// property value for the requested property. This is used, so that
/// reflection can be avoided for performance reasons. The easiest way
/// is to provide a lambda expression like this:<br/>
/// <code>(item) => item.MyProperty</code><br/>
/// This expression will simply return the value of the
/// <c>MyProperty</c> property of the passed item.</param>
/// <param name="nonSharedValue">The value which should be returned if
/// the values are not equal for all items.</param>
/// <returns>If all <typeparamref name="TItem"/> instances have
/// the same value for the specific property, this value will
/// be returned. If the values differ, <paramref name="nonSharedValue"/>
/// will be returned.</returns>
public static TProperty GetSharedValue<TItem, TProperty>(IList<TItem> items, Func<TItem, TProperty> getPropertyDelegate, TProperty nonSharedValue)
{
if (items == null || items.Count == 0)
return nonSharedValue;
TProperty sharedValue = getPropertyDelegate(items[0]);
for (int i = 1; i < items.Count; i++)
{
TItem currentItem = items[i];
TProperty currentValue = getPropertyDelegate(currentItem);
if (!sharedValue.Equals(currentValue))
return nonSharedValue;
}
return sharedValue;
}
#endregion
#region SetSharedValue<TItem, TProperty>(IList<TItem> a_items, Action<TItem, TProperty> a_setPropertyDelegate, TProperty a_newValue)
/// <summary>
/// Sets the same value for all <typeparamref name="TItem"/>
/// instances in <paramref name="a_items"/>.
/// </summary>
/// <typeparam name="TItem">The type of the items for which a shared
/// property value is requested.</typeparam>
/// <typeparam name="TProperty">The type of the property for which
/// a shared value is requested.</typeparam>
/// <param name="items">The collection of <typeparamref name="TItem"/>
/// instances for which a shared value should be set.</param>
/// <param name="setPropertyDelegate">A delegate which sets the
/// property value for the requested property. This is used, so that
/// reflection can be avoided for performance reasons. The easiest way
/// is to provide a lambda expression like this:<br/>
/// <code>(item, newValue) => item.MyProperty = newValue</code><br/>
/// This expression will simply set the value of the
/// <c>MyProperty</c> property of the passed item to <c>newValue</c>.</param>
/// <param name="newValue">The new value for the property.</param>
public static void SetSharedValue<TItem, TProperty>(IList<TItem> items, Action<TItem, TProperty> setPropertyDelegate, TProperty newValue)
{
if (items == null || items.Count == 0)
return;
foreach (TItem item in items)
{
try
{
setPropertyDelegate(item, newValue);
}
catch (Exception ex)
{
// log/error message here
}
}
}
#endregion
#endregion
}

Related

How To Accommodate ItemsSource and Items

Controls such as the ListBox have an ItemsSource property which you can bind to
<ListBox x:Name="ListBoxColours" ItemsSource="{Binding Colours}" />
It also has an Items property however, which can be used to add items in the code behind
ListBoxColours.Items.Add("Red");
I'm creating a CustomControl which has a ListBox in. I've exposed the ItemSource in my control to allow the user to bind the items to a property in their ViewModel.
<ListBox
x:Name="PART_ListBox"
ItemsSource="{Binding ItemsSource, RelativeSource={RelativeSource AncestorType=local:TextBoxComboControl}}"
SelectionMode="Single" />
...
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
"ItemsSource", typeof(IEnumerable), typeof(TextBoxComboControl), new PropertyMetadata(default(IEnumerable)));
public IEnumerable ItemsSource
{
get { return (IEnumerable) GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
...
<local:TextBoxComboControl ItemsSource="{Binding Colours}" />
I would like to add the ability for a user to add items in the code behind as well though, incase they don't want to use a binding. I was wondering how the ItemSource / Items properties interact with eachother. In order to allow them to use either peoprty, I would have to bind the ListBox items to both properties within my control.
The ListBox derives from Selector which derives from ItemsControl .
If you look at the source code to ItemsControl:
https://referencesource.microsoft.com/#PresentationFramework/Framework/System/Windows/Controls/ItemsControl.cs,54366ee76d2f1106
http://www.dotnetframework.org/default.aspx/4#0/4#0/DEVDIV_TFS/Dev10/Releases/RTMRel/wpf/src/Framework/System/Windows/Controls/ItemsControl#cs/1458001/ItemsControl#cs
You can see:
/// <summary>
/// ItemsSource specifies a collection used to generate the content of
/// this control. This provides a simple way to use exactly one collection
/// as the source of content for this control.
/// </summary>
/// <remarks>
/// Any existing contents of the Items collection is replaced when this
/// property is set. The Items collection will be made ReadOnly and FixedSize.
/// When ItemsSource is in use, setting this property to null will remove
/// the collection and restore use to Items (which will be an empty ItemCollection).
/// When ItemsSource is not in use, the value of this property is null, and
/// setting it to null has no effect.
/// </remarks>
[Bindable(true), CustomCategory("Content")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public IEnumerable ItemsSource
{
get { return Items.ItemsSource; }
set
{
if (value == null)
{
ClearValue(ItemsSourceProperty);
}
else
{
SetValue(ItemsSourceProperty, value);
}
}
}
If you look at the Items property which is of type ItemsCollection ... it can be in one of two modes (ItemsSource mode or "direct" mode).
/// <summary>
/// ItemCollection will contain items shaped as strings, objects, xml nodes,
/// elements, as well as other collections. (It will not promote elements from
/// contained collections; to "flatten" contained collections, assign a
/// <seealso cref="System.Windows.Data.CompositeCollection"/> to
/// the ItemsSource property on the ItemsControl.)
/// A <seealso cref="System.Windows.Controls.ItemsControl"/> uses the data
/// in the ItemCollection to generate its content according to its ItemTemplate.
/// </summary>
/// <remarks>
/// When first created, ItemCollection is in an uninitialized state, neither
/// ItemsSource-mode nor direct-mode. It will hold settings like SortDescriptions and Filter
/// until the mode is determined, then assign the settings to the active view.
/// When uninitialized, calls to the list-modifying members will put the
/// ItemCollection in direct mode, and setting the ItemsSource will put the
/// ItemCollection in ItemsSource mode.
/// </remarks>
There's an internal member called _isUsingItemsSource which gets set/cleared to track which mode it is in - this makes various method/properties behave differently depending on the mode.
"Items" are accessed via a CollectionView (which is kept in the _collectionView member) - this either points to an InnerItemCollectionView which wraps access to the direct items , or a CollectionView created with CollectionViewSource.GetDefaultCollectionView when SetItemsSource is called due to an "itemssource" being set.
You are most probably deriving from Control so you need to provide the similar behaviour. Maybe you can derive from ItemsControl to gain that behaviour....depends on your control of course if that is suitable.

ObservableCollection<EF4Entity> bound to ListView, but relation fields don't cause ListView to update

I have an ObservableCollection of Entity Framework 4 entities bound to a ListView. If I modify any of the normal, scalar properties of the entity, the values displayed in the ListView are updated.
Any relationship (navigation) properties are not updated in the ListView if they change, because the entity object doesn't implement change notifications for these properties.
Right now, I'm removing the entity from the collection and then reinserting it back into the same position to force the ListView to update.
There must be a better solution. What is it, if it exists?
Here's the generated code from VS2010's EF designer:
[EdmEntityTypeAttribute(NamespaceName="RovingAuditDb", Name="AuditRecord")]
[Serializable()]
[DataContractAttribute(IsReference=true)]
public partial class AuditRecord : EntityObject
{
#region Factory Method
/// <summary>
/// Create a new AuditRecord object.
/// </summary>
/// <param name="id">Initial value of the Id property.</param>
/// <param name="date">Initial value of the Date property.</param>
public static AuditRecord CreateAuditRecord(global::System.Int32 id, global::System.DateTime date)
{
AuditRecord auditRecord = new AuditRecord();
auditRecord.Id = id;
auditRecord.Date = date;
return auditRecord;
}
#endregion
#region Primitive Properties
// Deleted for this post
#endregion
#region Navigation Properties
/// <summary>
/// No Metadata Documentation available.
/// </summary>
[XmlIgnoreAttribute()]
[SoapIgnoreAttribute()]
[DataMemberAttribute()]
[EdmRelationshipNavigationPropertyAttribute("RovingAuditDb", "AuditRecordCell", "Cell")]
public Cell Cell
{
get
{
return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Cell>("RovingAuditDb.AuditRecordCell", "Cell").Value;
}
set
{
((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Cell>("RovingAuditDb.AuditRecordCell", "Cell").Value = value;
}
}
/// <summary>
/// No Metadata Documentation available.
/// </summary>
[BrowsableAttribute(false)]
[DataMemberAttribute()]
public EntityReference<Cell> CellReference
{
get
{
return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Cell>("RovingAuditDb.AuditRecordCell", "Cell");
}
set
{
if ((value != null))
{
((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedReference<Cell>("RovingAuditDb.AuditRecordCell", "Cell", value);
}
}
}
// Rest of the Navigation properties removed for this post
Those entities are not suitable for WPF two-way binding, which is based in INotifyPropertyChanged. I suggest you take a look at Self Tracking Entities, which are best suited for use in client/server type WPF application.

ComboBox not changing when bound value of SelectedItem 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!

WPF Binding to Combo Box

Hi a quick question really in WPF.
Does anyone know how i can bind a combo box that will sit in a grid to a dataset that i have.
The dataset contains several columns and numerious rows but i want to take one of the columns, lets say ProcessID and just show that in the comobo box.
Has anyone done this before or know how it can be done in WPF.
Thanks
Iffy.
Assuming this is a capital D DataSet that you can expose to XAML as a property on a DataContext object (showing the "Description" column for each row in the "LookupTable" table):
<ComboBox ItemsSource="{Binding Path=MyDataSetExposedAsAProperty.Tables[LookupTable].Rows}"
DisplayMemberPath=".[Description]"/>
To do it programmatically, here's a generic function I have for it:
/// <summary>
/// Thread safe method for databinding the specified ComboBox with the specified data.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="ctrl">The Combo to be databound.</param>
/// <param name="datasource">The data to be bound to the Combo.</param>
/// <param name="displayPath">The name of the property in the data objects that should be used for the display value.</param>
/// <param name="valuePath">The name of the property in the data objects that should be used for the value.</param>
/// <remarks>
/// This function was written as generic so that it doesn't matter what types the IEnumerabe datasource is, it can handle them all.
/// </remarks>
private void UpdateComboDataSource<T>(ComboBox ctrl, IEnumerable<T> datasource, string displayPath, string valuePath)
{
if (ctrl == null)
throw new ArgumentNullException("Control to be bound must not be null");
//check if we are on the control's UI thread:
if (ctrl.Dispatcher.CheckAccess())
{
//we have full access to the control, no threading issues, so let's rip it up and databind it
datasource = datasource.OrderBy(x => x);
if (displayPath != null && ctrl.DisplayMemberPath == null)
ctrl.DisplayMemberPath = displayPath;
if (valuePath != null && ctrl.SelectedValuePath == null)
ctrl.SelectedValuePath = valuePath;
ctrl.ItemsSource = datasource;
//be nice to the user, if there is only one item then automatically select it
ctrl.SelectedIndex = datasource.Count() == 1 ? 0 : -1;
}
else
{
//we don't have full access to the control because we are running on a different thread, so
//we need to marshal a call to this function from the control's UI thread
UpdateComboDataSourceDelegate<T> del = new UpdateComboDataSourceDelegate<T>(this.UpdateComboDataSource);
ctrl.Dispatcher.BeginInvoke(del, ctrl, datasource, displayPath, valuePath);
}
}
private delegate void UpdateComboDataSourceDelegate<T>(ComboBox ctrl, IEnumerable<T> dataSource, string displayPath, string valuePath);
The comments tell you everything you need to know. It could also be done programmatically by creating a Binding between the control's ItemSource property and a public property on your page - this is the same way as it is done declaratively (there must be a billion examples of that out there though, so I won't show it here).
You should use LINQ to SQL,ADO.Net, or entity framework to extract data from database, you can't bind combobox with database table, but whit data in the memory

wpf ComboBox Async binding to items source problem

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

Resources