views:

188

answers:

3

Hi,

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.

A: 

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.

ScoobyDon
Thanks for your reply! However, I have already had a solution now. I have posted my design in another answer to this question. You can find it [here](http://stackoverflow.com/questions/2468710/how-to-implement-master-detail-with-multi-selection-in-wpf/3388661#3388661).
gehho
+1  A: 

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

Captain
Actually, this is exactly the way that I chose. I have created a helper class called `SharedValueHelper` which I will post in an answer to this question in a minute...
gehho
You can now find my design [here](http://stackoverflow.com/questions/2468710/how-to-implement-master-detail-with-multi-selection-in-wpf/3388661#3388661).
gehho
A: 

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

}
gehho