Вопрос по listbox, wpf, c#, mvvm – Как поддерживать привязку ListBox SelectedItems с MVVM в навигационном приложении

30

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

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

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

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

ах, я вижу, вы уже пытаетесь использовать поведение. используйте BindableCollection для выбранных элементов, он должен работать. Если у вас есть больше проблем, просто дайте мне знать. Опишите их, и мы посмотрим. Mare Infinitus
Пожалуйста, покажите некоторый код, особенно SelectedItems и XAML. Является ли SelectedItems свойством? Подозреваю, что такое поведение, когда SelectedItems был просто открытым членом BindableCollection, а не Property. Mare Infinitus
Ах, я не понял, что это свойство должно было быть явно вызваноSelectedItems (мой называлсяSelectedLanguages). Теперь я получаюInvalidOperationException бросили вBindableCollection конструктор, когда я нажимаю кнопку & quot; Назад & quot; Кнопка в строке диспетчер вызывается сRaisePropertyChangedEventHandler, Я попытался просто вставить блок try / catch сDispatcher.BeginInvoke в блоке перехвата, но затем элементы списка не переизбираются при возврате к странице. caseklim

Ваш Ответ

9   ответов
47

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

<Style TargetType="{x:Type ListBoxItem}">
    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
@EduardoBrites Это не имеет значения, если вашIsSelected значение хранится наDataContext
@hypehuman Это, вероятно, связано с виртуализацией, когда пользовательский интерфейс будет отображать только видимые объекты (и несколько дополнительных для буфера прокрутки) и просто изменяет DataContext за элементами. Таким образом, в вашем SelectAll вам нужно убедиться, что все элементы данных выбраны, поскольку циклический просмотр всех элементов пользовательского интерфейса не является точным, поскольку создаются только видимые элементы.
Это работает, только если ListBox НЕ виртуализируется.
@hypehuman Это всегда вариант, но помните, что могут возникнуть проблемы с производительностью, если у вас большой набор данных.
@Rachel - что вы думаете о добавлении таких атрибутов, какIsSelected моделировать объекты? Я сделал это и немного запутался, особенно когда один и тот же экземпляр модельного объекта появляется в нескольких местах (в дереве). Когда один из них выбран, все они отображаются выбранными. Это не всегда желательно. Также мне кажется, что модель "загрязнена". По материалам просмотра. Какой у вас опыт?
0

Вот еще одно решение. Он подобен бенуответ, но привязка работает двумя способами. Хитрость в том, чтобы обновить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 свойство изменить на пустой список. Это, в свою очередь, приводит к тому, что свойство модели представлени измен етс на пустой список. В зависимости от вашего варианта использования это может быть нежелательно.

0

Это было серьезной проблемой для меня, некоторые из ответов, которые я видел, были либо слишком хакерскими, либо требовали перезагрузки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}"
/>

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

@kenny Мой пример не устанавливает начальное значение, но его можно легко добавить, мне просто не нужно было его использовать.
Мне бы очень понравилось, чтобы этот работал, но это не так. Это до определенной степени. Когда моя форма открывается, значения, которые должны быть выбраны, не выбраны в списке. Однако, если я нажму на значение, оно будет добавлено. Если я добавлю и удалю значение, которое должно было быть показано, оно будет удалено.
0

Не удовлетворенный данными ответами, я пытался найти один сам ... Ну, это больше похоже на взлом, чем решение, но для меня это работает отлично. Это решение использует 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");
            }
        }
    }

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

17

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

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

Примечание настройкиBasedOn (увидетьэтот ответ) .

-1

Оказывается, связывание флажка со свойством IsSelected и помещение текстового блока и флажка в панель стека делает свое дело!

8

Я не мог заставить Рэйчел работать так, как я хотел, но я нашелСандеш & APOS; s ответ на создание кастомасвойство зависимости работать идеально для меня. Мне просто нужно было написать похожий код для 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));

}

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

Мне нравится этот ответ, но я бы, вероятно, подправил код так:pastebin.com/YTccwmxG
@Moumit подходит для односторонней привязки, но можно ли ее адаптировать для использования с ObservableCollection & lt; T & gt; а сделал два пути?
В связи с тем, что модель не является воспитательной собственностью. Можете ли вы, пожалуйста, Sharethe XAML тоже
2

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

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

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

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

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

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

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

Тад. Готово.

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

0

Это было довольно легко сделать с помощью Command и Interactivities EventTrigger. ItemsCount - это просто привязанное свойство для использования в вашем XAML, если вы хотите отобразить обновленное количество.

XAML:

     <ListBox ItemsSource="{Binding SomeItemsSource}"
                 SelectionMode="Multiple">
        <i:Interaction.Triggers>
         <i:EventTrigger EventName="SelectionChanged">
            <i:InvokeCommandAction Command="{Binding SelectionChangedCommand}" 
                                   CommandParameter="{Binding ElementName=MyView, Path=SelectedItems.Count}" />
         </i:EventTrigger>
        </Interaction.Triggers>    
    </ListView>

<Label Content="{Binding ItemsCount}" />

ViewModel:

    private int _itemsCount;
    private RelayCommand<int> _selectionChangedCommand;

    public ICommand SelectionChangedCommand
    {
       get {
                return _selectionChangedCommand ?? (_selectionChangedCommand = 
             new RelayCommand<int>((itemsCount) => { ItemsCount = itemsCount; }));
           }
    }

        public int ItemsCount
        {
            get { return _itemsCount; }
            set { 
              _itemsCount = value;
              OnPropertyChanged("ItemsCount");
             }
        }

Похожие вопросы