Подтвердить что ты не робот

Синхронизировать SelectedItems в списке мулиселек с коллекцией в ViewModel

У меня есть список для множественного выбора в приложении SL3 с использованием призмы, и мне нужна коллекция в моей модели просмотра, которая содержит выбранные в данный момент элементы в списке.

Viewmodel ничего не знает о представлении, поэтому он не имеет доступа к элементу управления списком. Также мне нужно очистить выбранные элементы в списке из режима просмотра.

Не знаете, как подойти к этой проблеме.

спасибо Майкл

4b9b3361

Ответ 1

Итак, предположим, что у вас есть ViewModel со следующими свойствами:

public ObservableCollection<string> AllItems { get; private set; }
public ObservableCollection<string> SelectedItems { get; private set; }

Вы начнете с привязки своей коллекции AllItems к ListBox:

<ListBox x:Name="MyListBox" ItemsSource="{Binding AllItems}" SelectionMode="Multiple" />

Проблема заключается в том, что свойство SelectedItems в ListBox не является DependencyProperty. Это довольно плохо, так как вы не можете привязать его к чему-то в своей ViewModel.

Первый подход состоит в том, чтобы просто поместить эту логику в код, чтобы настроить ViewModel:

public MainPage()
{
    InitializeComponent();

    MyListBox.SelectionChanged += ListBoxSelectionChanged;
}

private static void ListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var listBox = sender as ListBox;
    if(listBox == null) return;

    var viewModel = listBox.DataContext as MainVM;
    if(viewModel == null) return;

    viewModel.SelectedItems.Clear();

    foreach (string item in listBox.SelectedItems)
    {
        viewModel.SelectedItems.Add(item);
    }
}

Этот подход будет работать, но он действительно уродлив. Мой предпочтительный подход заключается в том, чтобы извлечь это поведение в "Привязанное поведение". Если вы это сделаете, вы можете полностью устранить свой код и настроить его в XAML. Бонус в том, что это "привязанное поведение" теперь можно использовать повторно в любом ListBox:

<ListBox ItemsSource="{Binding AllItems}" Demo:SelectedItems.Items="{Binding SelectedItems}" SelectionMode="Multiple" />

И вот код для Прикрепленного Поведения:

public static class SelectedItems
{
    private static readonly DependencyProperty SelectedItemsBehaviorProperty =
        DependencyProperty.RegisterAttached(
            "SelectedItemsBehavior",
            typeof(SelectedItemsBehavior),
            typeof(ListBox),
            null);

    public static readonly DependencyProperty ItemsProperty = DependencyProperty.RegisterAttached(
            "Items",
            typeof(IList),
            typeof(SelectedItems),
            new PropertyMetadata(null, ItemsPropertyChanged));

    public static void SetItems(ListBox listBox, IList list) { listBox.SetValue(ItemsProperty, list); }
    public static IList GetItems(ListBox listBox) { return listBox.GetValue(ItemsProperty) as IList; }

    private static void ItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = d as ListBox;
        if (target != null)
        {
            GetOrCreateBehavior(target, e.NewValue as IList);
        }
    }

    private static SelectedItemsBehavior GetOrCreateBehavior(ListBox target, IList list)
    {
        var behavior = target.GetValue(SelectedItemsBehaviorProperty) as SelectedItemsBehavior;
        if (behavior == null)
        {
            behavior = new SelectedItemsBehavior(target, list);
            target.SetValue(SelectedItemsBehaviorProperty, behavior);
        }

        return behavior;
    }
}

public class SelectedItemsBehavior
{
    private readonly ListBox _listBox;
    private readonly IList _boundList;

    public SelectedItemsBehavior(ListBox listBox, IList boundList)
    {
        _boundList = boundList;
        _listBox = listBox;
        _listBox.SelectionChanged += OnSelectionChanged;
    }

    private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        _boundList.Clear();

        foreach (var item in _listBox.SelectedItems)
        {
            _boundList.Add(item);
        }
    }
}

Ответ 2

Я хотел иметь истинную двустороннюю привязку, чтобы выбор ListBox отражал элементы, содержащиеся в коллекции SelectedItems базового ViewModel. Это позволяет мне управлять выбором по логике в слое ViewModel.

Вот мои модификации класса SelectedItemsBehavior. Они синхронизируют коллекцию ListBox.SelectedItems с базовым свойством ViewModel, если свойство ViewModel реализует INotifyCollectionChanged (например, реализуемое типом ObservableCollection <T> ).

  public static class SelectedItems
  {
    private static readonly DependencyProperty SelectedItemsBehaviorProperty =
        DependencyProperty.RegisterAttached(
            "SelectedItemsBehavior",
            typeof(SelectedItemsBehavior),
            typeof(ListBox),
            null);

    public static readonly DependencyProperty ItemsProperty = DependencyProperty.RegisterAttached(
            "Items",
            typeof(IList),
            typeof(SelectedItems),
            new PropertyMetadata(null, ItemsPropertyChanged));

    public static void SetItems(ListBox listBox, IList list) { listBox.SetValue(ItemsProperty, list); }
    public static IList GetItems(ListBox listBox) { return listBox.GetValue(ItemsProperty) as IList; }

    private static void ItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      var target = d as ListBox;
      if (target != null)
      {
        AttachBehavior(target, e.NewValue as IList);
      }
    }

    private static void AttachBehavior(ListBox target, IList list)
    {
      var behavior = target.GetValue(SelectedItemsBehaviorProperty) as SelectedItemsBehavior;
      if (behavior == null)
      {
        behavior = new SelectedItemsBehavior(target, list);
        target.SetValue(SelectedItemsBehaviorProperty, behavior);
      }
    }
  }

  public class SelectedItemsBehavior
  {
    private readonly ListBox _listBox;
    private readonly IList _boundList;

    public SelectedItemsBehavior(ListBox listBox, IList boundList)
    {
      _boundList = boundList;
      _listBox = listBox;
      _listBox.Loaded += OnLoaded;
      _listBox.DataContextChanged += OnDataContextChanged;
      _listBox.SelectionChanged += OnSelectionChanged;

      // Try to attach to INotifyCollectionChanged.CollectionChanged event.
      var notifyCollectionChanged = boundList as INotifyCollectionChanged;
      if (notifyCollectionChanged != null)
      {
        notifyCollectionChanged.CollectionChanged += OnCollectionChanged;
      }
    }

    void UpdateListBoxSelection()
    {
      // Temporarily detach from ListBox.SelectionChanged event
      _listBox.SelectionChanged -= OnSelectionChanged;

      // Synchronize selected ListBox items with bound list
      _listBox.SelectedItems.Clear();
      foreach (var item in _boundList)
      {
        // References in _boundList might not be the same as in _listBox.Items
        var i = _listBox.Items.IndexOf(item);
        if (i >= 0)
        {
          _listBox.SelectedItems.Add(_listBox.Items[i]);
        }
      }

      // Re-attach to ListBox.SelectionChanged event
      _listBox.SelectionChanged += OnSelectionChanged;
    }

    void OnLoaded(object sender, RoutedEventArgs e)
    {
      // Init ListBox selection
      UpdateListBoxSelection();
    }

    void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
      // Update ListBox selection
      UpdateListBoxSelection();
    }

    void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
      // Update ListBox selection
      UpdateListBoxSelection();
    }

    void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
      // Temporarily deattach from INotifyCollectionChanged.CollectionChanged event.
      var notifyCollectionChanged = _boundList as INotifyCollectionChanged;
      if (notifyCollectionChanged != null)
      {
        notifyCollectionChanged.CollectionChanged -= OnCollectionChanged;
      }

      // Synchronize bound list with selected ListBox items
      _boundList.Clear();
      foreach (var item in _listBox.SelectedItems)
      {
        _boundList.Add(item);
      }

      // Re-attach to INotifyCollectionChanged.CollectionChanged event.
      if (notifyCollectionChanged != null)
      {
        notifyCollectionChanged.CollectionChanged += OnCollectionChanged;
      }
    }
  }

Ответ 3

Спасибо за это! Я добавил небольшое обновление для поддержки начальной загрузки и изменения DataContext.

Приветствия,

Алессандро Пилотти [MVP/IIS]

public class SelectedItemsBehavior
{
    private readonly ListBox _listBox;
    private readonly IList _boundList;

    public ListBoxSelectedItemsBehavior(ListBox listBox, IList boundList)
    {
        _boundList = boundList;
        _listBox = listBox;

        SetSelectedItems();

        _listBox.SelectionChanged += OnSelectionChanged;
        _listBox.DataContextChanged += ODataContextChanged;
    }

    private void SetSelectedItems()
    {
        _listBox.SelectedItems.Clear();

        foreach (object item in _boundList)
        {
            // References in _boundList might not be the same as in _listBox.Items
            int i = _listBox.Items.IndexOf(item);
            if (i >= 0)
                _listBox.SelectedItems.Add(_listBox.Items[i]);
        }
    }

    private void ODataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        SetSelectedItems();
    }

    private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        _boundList.Clear();

        foreach (var item in _listBox.SelectedItems)
        {
            _boundList.Add(item);
        }
    }
}

Ответ 5

Исходное решение выше работает, если вы сначала запомните экземпляр наблюдаемой коллекции! Кроме того, вам необходимо убедиться, что тип содержимого коллекции Observable соответствует типу содержимого для вашего элемента списка ListBox (если вы отклоняетесь от точного примера, упомянутого выше).

Ответ 6

Вот блог с решением этой проблемы, включая пример приложения, чтобы вы могли точно увидеть, как заставить его работать: http://alexshed.spaces.live.com/blog/cns!71C72270309CE838!149.entry

Я только что реализовал это в своем приложении и прекрасно решает проблему

Ответ 7

Решение для меня состояло в том, чтобы объединить обновление Алессандро Пилотти с пристрастным поведением Брайана Генисио. Но удалите код для DataContext, изменяя Silverlight 4, не поддерживает это.

Если вы привязываете список к ObservableCollection<string>, это работает отлично, но если вы привязываетесь к сложным объектам типа ObservableCollection<Person> SelectedItems { get; private set; } через DataTemplate, похоже, что это не работает. Это связано с реализацией по умолчанию метода Equals, который использует коллекция. Вы можете решить эту проблему, сообщив объекту Person, какие поля следует сравнивать при определении того, являются ли объекты равными, это выполняется путем реализации интерфейса IEquatable<T> на вашем объекте.

После этого код IndexOf (item) будет работать и сможет сравниться, если объекты равны и выбрать элемент в списке

// References in _boundList might not be the same as in _listBox.Items
int i = _listBox.Items.IndexOf(item);
if (i >= 0)
  _listBox.SelectedItems.Add(_listBox.Items[i]);

См. ссылку: http://msdn.microsoft.com/en-us/library/ms131190(VS.95).aspx

Ответ 8

Im, использующий объект EventToCommand для выбора измененного события в XAML и передачи там ListBox в качестве параметра. Затем команда в MMVM управляет ObservableCollection для выбранных элементов. Его легко и быстро;)

Ответ 9

Решение Брайана Генисио и Самуэля Джека здесь отлично. Я успешно его реализовал. Но у меня также был случай, когда это не сработало, и с тех пор я не являюсь экспертом в WPF или .NET, я не смог его отладить. Я до сих пор не уверен, в чем проблема, но в свое время я нашел обходное решение для привязки к мультиселекту. И в этом решении мне не пришлось попасть в DataContext.

Это решение предназначено для людей, которые не смогли заставить вышеуказанные 2 решения работать. Думаю, это решение не будет считаться MVVM. Все идет так. Предположим, у вас есть 2 коллекции в ViewModel:

public ObservableCollection<string> AllItems { get; private set; }
public ObservableCollection<string> SelectedItems { get; private set; }

Вам нужен поле "Список":

<ListBox x:Name="MyListBox" ItemsSource="{Binding AllItems}" SelectionMode="Multiple" />

Теперь добавьте еще один ListBox и привяжите его к SelectedItems и установите видимость:

<ListBox x:Name="MySelectedItemsListBox" ItemsSource="{Binding SelectedItems, Mode=OneWayToSource}" SelectionMode="Multiple" Visibility="Collapsed" />

Теперь, в коде позади страницы WPF, добавьте конструктор после метода InitializeComponent():

MyListBox.SelectionChanged += MyListBox_SelectionChanged;

И добавьте метод:

private void MyListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    MySelectedItemsListBox.ItemsSource = MyListBox.SelectedItems;
}

И все готово. Это будет работать точно. Я думаю, это можно использовать и в Silverlight, если это решение не работает.

Ответ 10

Для тех, кто еще не смог заставить candritzky ответить на работу, убедитесь, что вы не изменили цвета темы Windows, такие как я. Оказывается, мой цвет фона ListBox соответствовал цвету выбора, когда ListBox был не в фокусе, тем самым он выглядел так, как будто ничего не было выбрано.

Измените кисть фона ListBox на красный, чтобы проверить, так ли это происходит с вами. Это заставило меня провести 2 часа, пока я не осознал...