MVVM ListView MultiBinding SelectedItems + SelectedItem (ListView) + SelectedItem (ComboBox) для TextBox.Text. Не обновляется должным образом

Я пытаюсь MultiBind SelectedItems (ListView) и SelectedItem или SelectedCategory в моей ViewModel (ComboBox) только для чтения TextBox, OutputConverter просто проверяет, выбран ли хотя бы один элемент (ListView) и TypeData (ComboBox) перед созданием текста для TextBox,

Однако, если я попробую только с этим TextBox только обновления, когда ComboBox.SelectedItem меняется не когда SelectedItems внутри ListView меняется.
Поэтому я также включил из моей ViewModel SelectedEntry (ListView) (который такой же, как SelectedItem) в качестве привязки для мультисвязывания.

Теперь я получаю следующее:

Объяснение:
Выбор всегда на один шаг позади и использует предыдущий SelectedItems из ListView для привязки к TextBox.Text, Даже когда я выбираю несколько записей через CTRL или Shift + клик. Однако если ComboBox.SelectedItem изменяет это обновляет TextBox как предполагалось.

Как получить поведение, если выбор изменяется внутри ListView TextBox немедленно обновляет его содержимое (желательно, чтобы я использовал SelectedEntries моей ViewModel, а не SelectedItems ListView и, если возможно, в соответствии с MVVM)?

Редактировать:

  • Я заметил, что когда конвертер называется SelectedEntry (реализует INotifyPropertyChanged), он обновляется, а SelectedEntries (ObservableCollection и реализует INotifyPropertyChanged) - нет.
  • Я также включил событие SelectionChanged, которое вызывает команду и передает SelectedItems в качестве параметра в команду. Если это может помочь, но я бы хотел иметь правильную привязку, которая обновляется соответственно.

Код:

Модель TypeData (ComboBox):

public class TypeData : INotifyPropertyChanged
{
    public enum Type
    {
        NotSet = '0',
        A = 'A',
        B = 'B',
        C = 'C'
    }

    private string name;

    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            //OnPropertyChanged("Name");
            OnPropertyChanged(nameof(Name));
        }
    }
    private Type category;

    public Type Category
    {
        get { return category; }
        set { category = value; }
    }


    public TypeData(string name)
    {
        Name = name;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;

        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public override string ToString()
    {
        return Name;
    }
}

Модель входа (ListView):

public class Entry : INotifyPropertyChanged
{
    private string title;
    public string Title
    {
        get { return title; }
        set
        {
            title = value;
            OnPropertyChanged(nameof(Title));
        }
    }

    private string author;

    public string Author
    {
        get { return author; }
        set
        {
            author = value;
            OnPropertyChanged(nameof(Author));
        }
    }

    private string year;

    public string Year
    {
        get { return year; }
        set
        {
            year = value;
            OnPropertyChanged(nameof(Year));
        }
    }


    public Entry(string title, string author, string year)
    {
        Title = title;
        Author = author;
        Year = year;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;

        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

ViewModel:

public class MainViewModel
{
    public ObservableCollection<Entry> Entries { get; set; }

    public Entry SelectedEntry { get; set; }

    public ObservableCollection<Entry> SelectedEntries { get; set; }

    public ObservableCollection<TypeData> Types { get; set; }

    private TypeData selectedCategory;

    public TypeData SelectedCategory { get; set; }

    public RelayCommand<object> SelectionChangedCommand { get; set; }

    public MainViewModel()
    {
        Entries = new ObservableCollection<Entry>
        {
            new Entry("Title1", "Author1", "Year1"),
            new Entry("Title2", "Author2", "Year2"),
            new Entry("Title3", "Author3", "Year3"),
            new Entry("Title4", "Author4", "Year4"),
        };

        Types = new ObservableCollection<TypeData>
        {
            new TypeData("A"),
            new TypeData("B"),
            new TypeData("C"),
        };

        SelectionChangedCommand = new RelayCommand<object>(items =>
        {
            var selectedEntries = (items as ObservableCollection<object>).Cast<Entry>();
            SelectedEntries = new ObservableCollection<Entry>(selectedEntries);
        });
    }
}

XAML:

<Window x:Class="MvvmMultiBinding.View.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:MvvmMultiBinding"
    xmlns:m="clr-namespace:MvvmMultiBinding.Model"
    xmlns:vm="clr-namespace:MvvmMultiBinding.ViewModel"
    xmlns:conv="clr-namespace:MvvmMultiBinding.View.Converter"
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    mc:Ignorable="d"
    Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
    <vm:MainViewModel></vm:MainViewModel>
</Window.DataContext>
<Window.Resources>
    <conv:OutputConverter x:Key="OutputConverter"/>
</Window.Resources>
<Grid>
    <DockPanel>
        <ListView Name="ListViewEntries" ItemsSource="{Binding Entries}" SelectedItem="{Binding SelectedEntry}" DockPanel.Dock="Top">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Title" Width="250" DisplayMemberBinding="{Binding Title}" />
                    <GridViewColumn Header="Author" Width="150" DisplayMemberBinding="{Binding Author}" />
                    <GridViewColumn Header="Year" Width="50" DisplayMemberBinding="{Binding Year}" />
                </GridView>
            </ListView.View>
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="SelectionChanged">
                    <i:InvokeCommandAction Command="{Binding DataContext.SelectionChangedCommand, ElementName=ListViewEntries}"
                                       CommandParameter="{Binding SelectedItems, ElementName=ListViewEntries}"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </ListView>
        <ComboBox ItemsSource="{Binding Types}" SelectedItem="{Binding SelectedCategory}" MinWidth="200" DockPanel.Dock="Right"/>
        <TextBox IsReadOnly="True" DockPanel.Dock="Left">
            <TextBox.Text>
                <MultiBinding Converter="{StaticResource OutputConverter}">
                    <Binding ElementName="ListViewEntries" Path="SelectedItems" Mode="OneWay"/>
                    <!--<Binding Path="SelectedEntries" Mode="OneWay"/>-->
                    <Binding Path="SelectedCategory" Mode="OneWay"/>
                    <!-- Without it converter is not called after selection changes -->
                    <Binding Path="SelectedEntry" Mode="OneWay"/>
                </MultiBinding>
            </TextBox.Text>
        </TextBox>
    </DockPanel> 
</Grid>

OutputConverter:

public class OutputConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        System.Collections.IList items = (System.Collections.IList)values[0];
        var entries = items.Cast<Entry>();
        TypeData type = values[1] as TypeData;
        List<Entry> selectedEntries = new List<Entry>();

        foreach (var entry in entries)
        {
            selectedEntries.Add(entry);
        }
        StringBuilder sb = new StringBuilder();

        // ComboBox and Selection must not be empty
        if (type != null && selectedEntries.Count > 0)
        {
            foreach (var selectedEntry in selectedEntries)
            {
                sb.AppendFormat("{0} {1}\n\n", selectedEntry.Author, type);
            }
        }

        return sb.ToString();
    }

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

1 ответ

Решение

Я бы предложил предоставить вашему классу входа свойство IsSelected (привязанное к IsSelectedProperty объекта ListViewItem). Когда выбор изменяется, вы можете просто перебрать свою коллекцию (привязанную к ListView) и проверить, выбраны они или нет. Вот так (извините за MvvmLight, RelayCommand = ICommand, ViewModelBase расширяет ObservableObject):

ViewModel:

public class MainViewModel : ViewModelBase
{
    public MainViewModel()
    {
        TestItemCollection = new ObservableCollection<TestItem>
        {
            new TestItem("Test1"),
             new TestItem("Test2"),
              new TestItem("Test3")
            };
    }

    private TestItem m_selectedItemProperty;
    public TestItem SelectedItemProperty
    {
        get
        {
            return m_selectedItemProperty;
        }
        set
        {
            m_selectedItemProperty = value;
            RaisePropertyChanged("SelectedItemProperty");
        }
    }

    public ObservableCollection<TestItem> TestItemCollection
    {
        get;
        set;
    }

    public RelayCommand SelectionChanged
    {
        get { return new RelayCommand(OnSelectionChanged); }
    }

    private void OnSelectionChanged()
    {
        foreach (var item in TestItemCollection)
        {
            if (item.IsSelected)
                Console.WriteLine("Name: " + item.Name);
        }
    }
}

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

TestItem:

public class TestItem : ObservableObject
{
    public TestItem(string a_name)
    {
        m_name = a_name;
    }

    private string m_name;
    public string Name
    {
        get
        {
            return m_name;
        }
        set
        {
            m_name = value;
            RaisePropertyChanged("Name");
        }
    }

    private bool m_isSelected;
    public bool IsSelected
    {
        get
        {
            return m_isSelected;
        }
        set
        {
            m_isSelected = value;
            RaisePropertyChanged("IsSelected");
        }
    }
}

Посмотреть:

<Window x:Class="WpfAppTests.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfAppTests"
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
    mc:Ignorable="d"
    xmlns:modelNoMvvmLight="clr-namespace:WpfAppTests"
    xmlns:modelMvvmLight="clr-namespace:WpfAppTests.ViewModel"
    Title="MainWindow" Height="350" Width="525" >
<Window.DataContext>
    <modelMvvmLight:MainViewModel/>
</Window.DataContext>

<StackPanel>
    <ListView Name="ListView" ItemsSource="{Binding TestItemCollection}" SelectedItem="{Binding SelectedItemProperty}">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="SelectionChanged">
                <i:InvokeCommandAction Command="{Binding SelectionChanged}"/>
            </i:EventTrigger>
        </i:Interaction.Triggers>

        <ListView.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Name}"/>
            </DataTemplate>
        </ListView.ItemTemplate>

        <ListView.ItemContainerStyle>
            <Style TargetType="ListViewItem" >
                <Setter Property="IsSelected" Value="{Binding IsSelected}"/>
            </Style>
        </ListView.ItemContainerStyle>
    </ListView>
</StackPanel>
</Window>
Другие вопросы по тегам