Как реализовать Master-Detail с мульти-выделением в WPF? - PullRequest
2 голосов
/ 18 марта 2010

Я планирую создать типичный сценарий Master-Detail, т. Е. Набор элементов, отображаемых в ListView через привязку данных к ICollectionView, и подробности о выбранном элементе в отдельной группе элементов управления (TextBoxes, NumUpDowns). ..).

Пока проблем нет, на самом деле я уже реализовал довольно похожий сценарий в старом проекте. Однако должна быть возможность выбрать несколько элементов в ListView и получить соответствующие общие значения, отображаемые в подробном представлении. Это означает, что если все выбранные элементы имеют одинаковое значение для свойства, это значение должно отображаться в подробном представлении. Если они не разделяют одно и то же значение, соответствующий элемент управления должен предоставить некоторую визуальную подсказку для пользователя, указывающего на это, и никакое значение не должно отображаться (или состояние «неопределенное» в CheckBox, например). Теперь, если пользователь редактирует значение, это изменение должно применяться к всем выбранным элементам.

Дополнительные требования:

  • Совместимость с MVVM (т. Е. Не слишком много кода)
  • Расширяемость (новые свойства / типы могут быть добавлены позже)

Кто-нибудь имеет опыт работы с таким сценарием? На самом деле, я думаю, что это должен быть очень распространенный сценарий. Однако я нигде не смог найти подробностей на эту тему.

Спасибо!
gehho.

PS: В упомянутом выше старом проекте у меня было решение с использованием подкласса ViewModel, который обрабатывает особый случай множественного выбора. Он проверил все выбранные элементы на равенство и вернул соответствующие значения. Однако у этого подхода были некоторые недостатки, и он почему-то казался хаком, потому что (помимо других вонючих вещей) необходимо было прервать синхронизацию между ListView и подробным представлением и обработать его вручную.

Ответы [ 3 ]

1 голос
/ 02 августа 2010

В вашей ViewModel создайте свойство, которое будет привязано к SelectedItems вашего ListView.

Создайте еще одно свойство, которое будет представлять объект сведений о выбранных вами элементах.

Раздел сведений (в XAML) привязан к этому свойству сведений (в ViewModel).

Каждый раз, когда изменяется выбранная коллекция элементов (событие setter / CollectionChanged), вам также необходимо обновлять объект сведений (который будет перебирать соответствующие свойства и решать, имеют ли они одинаковое значение или нет).

После изменения свойства в объекте сведений необходимо выполнить итерацию обратно в выбранную коллекцию элементов и внести в них соответствующие изменения.

Вот и все.

Надеюсь, это поможет

1 голос
/ 02 августа 2010

Я сталкиваюсь с той же самой проблемой.

IsSynchronizedWithCurrentItem = "True" только синхронизирует CurrentItem (последний выбранный элемент без удержания Ctrl или Shift).

Как и вы, наверное, я искал ответ в интернете повсюду и не нашел его. В моем сценарии есть трехуровневая привязка Master> Detail> Detail, где каждый уровень связан со своим собственным ListBox.

Я установил кое-что, что работает на данный момент.

Для моих уровней Master> Detail> Detail я создал отдельный CollectionViewSource для каждого уровня и установил для этого CollectionViewSource.Source соответствующий объект сущности. В событии SelectionChanged, связанном с MasterView, я выполнил фильтрацию на MasterView.View, чтобы получить объекты, в которых главный первичный ключ = внешний ключ детали.

Это неряшливо, но если бы вы нашли лучший способ сделать это, я бы с удовольствием это услышал.

0 голосов
/ 02 августа 2010

Я использовал подход, аналогичный , предложенный капитаном . Я создал одно свойство в моей модели представления, которое представляет несколько элементов (то есть все выбранные элементы). В свойстве get- и set-accessors я использовал следующие методы для определения / установки общих значений of / для всех элементов. Этот подход не использует никакого отражения, но использует делегаты в форме лямбда-выражений, что делает его довольно быстрым.

В качестве обзора, это мой основной дизайн:

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)

}

Свойства выглядят так:

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);
    }
}

А код для класса SharedValueHelper выглядит так:

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

}
...