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>