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

Как поддерживать привязку ListBox SelectedItems к MVVM в навигационном приложении

Я создаю приложение WPF, которое можно использовать с помощью пользовательских кнопок "Далее" и "Назад" (например, не используя NavigationWindow). На одном экране у меня есть ListBox, который должен поддерживать множественный выбор (используя режим Extended). У меня есть модель представления для этого экрана и сохранение выбранных элементов в качестве свойства, поскольку их необходимо поддерживать.

Однако я знаю, что свойство SelectedItems для ListBox доступно только для чтения. Я пытался решить проблему, используя это решение здесь, но я не смог ее перенести в мою реализацию. Я обнаружил, что я не могу различать, когда один или несколько элементов отменены, и когда я перемещаюсь между экранами (NotifyCollectionChangedAction.Remove поднимается в обоих случаях, поскольку технически все выбранные элементы не выбираются при навигации по экрану). Мои навигационные команды расположены в отдельной модели представлений, которая управляет моделями просмотра для каждого экрана, поэтому я не могу помещать какую-либо реализацию, связанную с моделью просмотра, с ListBox там.

Я нашел несколько других менее элегантных решений, но ни один из них, похоже, не обеспечивает двухстороннюю привязку между моделью просмотра и представлением.

Любая помощь будет принята с благодарностью. Я могу предоставить часть своего исходного кода, если это поможет понять мою проблему.

4b9b3361

Ответ 1

Попробуйте создать свойство IsSelected для каждого из ваших элементов данных и привязать ListBoxItem.IsSelected к этому свойству

<Style TargetType="{x:Type ListBoxItem}">
    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>

Ответ 2

Решения Rachel отлично работают! Но есть одна проблема, с которой я столкнулся - если вы переопределите стиль ListBoxItem, вы потеряете оригинальный стиль, примененный к нему (в моем случае ответственный за выделение выделенного элемента и т.д.). Вы можете избежать этого, наследуя от оригинального стиля:

<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>

Установка примечаний BasedOn (см. этот ответ) .

Ответ 3

Я не мог заставить решение Rachel работать так, как я этого хотел, но нашел ответ Sandesh о создании пользовательского свойство зависимостей отлично работает для меня. Мне просто пришлось написать аналогичный код для ListBox:

public class ListBoxCustom : ListBox
{
    public ListBoxCustom()
    {
        SelectionChanged += ListBoxCustom_SelectionChanged;
    }

    void ListBoxCustom_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        SelectedItemsList = SelectedItems;
    }

    public IList SelectedItemsList
    {
        get { return (IList)GetValue(SelectedItemsListProperty); }
        set { SetValue(SelectedItemsListProperty, value); }
    }

    public static readonly DependencyProperty SelectedItemsListProperty =
       DependencyProperty.Register("SelectedItemsList", typeof(IList), typeof(ListBoxCustom), new PropertyMetadata(null));

}

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

Ответ 4

Я продолжал искать легкое решение для этого, но не повезло.

Решение Rachel хорошо, если у вас уже есть свойство Selected на объекте в вашем ItemsSource. Если вы этого не сделаете, вам нужно создать модель для этой бизнес-модели.

Я пошел другим путем. Быстрый, но не идеальный.

В вашем ListBox создайте событие для SelectionChanged.

<ListBox ItemsSource="{Binding SomeItemsSource}"
         SelectionMode="Multiple"
         SelectionChanged="lstBox_OnSelectionChanged" />

Теперь реализуйте событие на коде, расположенном за вашей страницей XAML.

private void lstBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var listSelectedItems = ((ListBox) sender).SelectedItems;
    ViewModel.YourListThatNeedsBinding = listSelectedItems.Cast<ObjectType>().ToList();
}

Тада. Готово.

Это было сделано с помощью преобразования SelectedItemCollection в список.

Ответ 5

Не удовлетворенными ответами, я пытался найти один сам... Ну, похоже, это скорее хак, а решение, но для меня это прекрасно работает. Это решение использует MultiBindings особым образом. Сначала это может показаться тонкой кода, но вы можете использовать его с минимальными усилиями.

Сначала я реализовал "IMultiValueConverter"

public class SelectedItemsMerger : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        SelectedItemsContainer sic = values[1] as SelectedItemsContainer;

        if (sic != null)
            sic.SelectedItems = values[0];

        return values[0];
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return new[] { value };
    }
}

И контейнер/обертка SelectedItems:

public class SelectedItemsContainer
{
    /// Nothing special here...
    public object SelectedItems { get; set; }
}

Теперь мы создаем привязку для нашего ListBox.SelectedItem(Singular). Примечание. Вам необходимо создать статический ресурс для "Конвертера". Это можно сделать один раз для каждого приложения и повторно использовать для всех списков, которые нуждаются в конвертере.

<ListBox.SelectedItem>
 <MultiBinding Converter="{StaticResource SelectedItemsMerger}">
  <Binding Mode="OneWay" RelativeSource="{RelativeSource Self}" Path="SelectedItems"/>
  <Binding Path="SelectionContainer"/>
 </MultiBinding>
</ListBox.SelectedItem>

В ViewModel я создал контейнер, с которым я могу привязываться. Важно инициализировать его с помощью new(), чтобы заполнить его значениями.

    SelectedItemsContainer selectionContainer = new SelectedItemsContainer();
    public SelectedItemsContainer SelectionContainer
    {
        get { return this.selectionContainer; }
        set
        {
            if (this.selectionContainer != value)
            {
                this.selectionContainer = value;
                this.OnPropertyChanged("SelectionContainer");
            }
        }
    }

И что это. Может быть, кто-то видит некоторые улучшения? Что вы думаете об этом?

Ответ 6

Вот еще одно решение. Это похоже на ответ Бена, но привязка работает двумя способами. Хитрость заключается в обновлении выбранных элементов ListBox при изменении связанных элементов данных.

public class MultipleSelectionListBox : ListBox
{
    public static readonly DependencyProperty BindableSelectedItemsProperty =
        DependencyProperty.Register("BindableSelectedItems",
            typeof(IEnumerable<string>), typeof(MultipleSelectionListBox),
            new FrameworkPropertyMetadata(default(IEnumerable<string>),
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));

    public IEnumerable<string> BindableSelectedItems
    {
        get => (IEnumerable<string>)GetValue(BindableSelectedItemsProperty);
        set => SetValue(BindableSelectedItemsProperty, value);
    }

    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        base.OnSelectionChanged(e);
        BindableSelectedItems = SelectedItems.Cast<string>();
    }

    private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is MultipleSelectionListBox listBox)
            listBox.SetSelectedItems(listBox.BindableSelectedItems);
    }
}

К сожалению, я не смог использовать IList как тип BindableSelectedItems. Это отправил null в мое свойство модели просмотра, тип которого IEnumerable<string>.

Здесь XAML:

<v:MultipleSelectionListBox
    ItemsSource="{Binding AllMyItems}"
    BindableSelectedItems="{Binding MySelectedItems}"
    SelectionMode="Multiple"
    />

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

Ответ 7

Для меня это было серьезной проблемой, некоторые из ответов, которые я видел, были слишком хакерскими или требовали сброса значения свойства SelectedItems нарушающего любой код, прикрепленный к объекту OnCollectionChanged. Но мне удалось получить работоспособное решение, изменив коллекцию напрямую, и в качестве бонуса он даже поддерживает SelectedValuePath для коллекций объектов.

public class MultipleSelectionListBox : ListBox
{
    internal bool processSelectionChanges = false;

    public static readonly DependencyProperty BindableSelectedItemsProperty =
        DependencyProperty.Register("BindableSelectedItems",
            typeof(object), typeof(MultipleSelectionListBox),
            new FrameworkPropertyMetadata(default(ICollection<object>),
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));

    public dynamic BindableSelectedItems
    {
        get => GetValue(BindableSelectedItemsProperty);
        set => SetValue(BindableSelectedItemsProperty, value);
    }


    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        base.OnSelectionChanged(e);

        if (BindableSelectedItems == null || !this.IsInitialized) return; //Handle pre initilized calls

        if (e.AddedItems.Count > 0)
            if (!string.IsNullOrWhiteSpace(SelectedValuePath))
            {
                foreach (var item in e.AddedItems)
                    if (!BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)))
                        BindableSelectedItems.Add((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
            }
            else
            {
                foreach (var item in e.AddedItems)
                    if (!BindableSelectedItems.Contains((dynamic)item))
                        BindableSelectedItems.Add((dynamic)item);
            }

        if (e.RemovedItems.Count > 0)
            if (!string.IsNullOrWhiteSpace(SelectedValuePath))
            {
                foreach (var item in e.RemovedItems)
                    if (BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)))
                        BindableSelectedItems.Remove((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
            }
            else
            {
                foreach (var item in e.RemovedItems)
                    if (BindableSelectedItems.Contains((dynamic)item))
                        BindableSelectedItems.Remove((dynamic)item);
            }
    }

    private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is MultipleSelectionListBox listBox)
        {
            List<dynamic> newSelection = new List<dynamic>();
            if (!string.IsNullOrWhiteSpace(listBox.SelectedValuePath))
                foreach (var item in listBox.BindableSelectedItems)
                {
                    foreach (var lbItem in listBox.Items)
                    {
                        var lbItemValue = lbItem.GetType().GetProperty(listBox.SelectedValuePath).GetValue(lbItem, null);
                        if ((dynamic)lbItemValue == (dynamic)item)
                            newSelection.Add(lbItem);
                    }
                }
            else
                newSelection = listBox.BindableSelectedItems as List<dynamic>;

            listBox.SetSelectedItems(newSelection);
        }
    }
}

Связывание работает так же, как вы ожидали, что MS сделает сами:

<uc:MultipleSelectionListBox 
    ItemsSource="{Binding Items}" 
    SelectionMode="Extended" 
    SelectedValuePath="id" 
    BindableSelectedItems="{Binding mySelection}"
/>

Он не был тщательно протестирован, но прошел проверку первого взгляда. Я пытался использовать его повторно, используя динамические типы в коллекциях.

Ответ 8

Выключает привязку флажка к свойству IsSelected, и размещение текстового блока и флажка в панели стека делает трюк!