it-swarm.com.ru

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

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

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

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

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

30
caseklim

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

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

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

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

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

17
Mifeet

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

}

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

8
Ben

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

Решение, которое есть у Рэйчел, хорошо, если у вас уже есть свойство 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 в Список .

2
AzzamAziz

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

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

0
Fresch

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

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

0
Wobbles

Это было довольно легко сделать с помощью 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");
             }
        }
0
str8ball

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