Я использую WPF DataGrid, привязанный к коллекции. Один из столбцов будет ComboBox. Я хочу, чтобы ComboBox был доступен для редактирования, а также чтобы любые изменения в тексте распространялись на все остальные элементы коллекции, которые ранее использовали то же значение.
Вот мой XAML:
<Window x:Class = "WpfApp1.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:WpfApp1"
mc:Ignorable = "d"
DataContext = "{Binding RelativeSource = {RelativeSource Self}}"
Title = "MainWindow" Height = "450" Width = "800">
<DataGrid AutoGenerateColumns = "False" ItemsSource = "{Binding Students}">
<DataGrid.Columns>
<DataGridTextColumn Header = "Name" Binding = "{Binding Name}" IsReadOnly = "True" />
<DataGridTemplateColumn Header = "Team Name">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox IsEditable = "True" ItemsSource = "{Binding RelativeSource = {RelativeSource FindAncestor, AncestorType = {x:Type Window}}, Path=TeamNames}" Text = "{Binding TeamName}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Window>
и вот мой код:
public class Student : INotifyPropertyChanged
{
private string teamName;
public string Name { get; set; }
public string TeamName
{
get { return teamName; }
set
{
teamName = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("TeamName"));
}
}
public Student(string name, string teamName)
{
Name = name;
this.teamName = teamName;
}
public event PropertyChangedEventHandler PropertyChanged;
}
public partial class MainWindow : Window
{
public ObservableCollection<Student> Students { get; set; } = new ObservableCollection<Student>(new Student[] { new Student("Alice", "Red"), new Student("Bob", "Blue"), new Student("Craig", "Blue") });
public List<string> TeamNames { get; set; } = new List<string>();
public MainWindow()
{
InitializeComponent();
Colors.AddRange(Students.Select(x => x.TeamName).Distinct());
}
}
Так, например, если название команды Крейга изменено на «BlueGreen», команда Боба должна обновиться одновременно:
Я подобрался довольно близко к этому, обработав событие ComboBox.TextBoxBase.TextChanged, но оно оказалось настолько сложным, что я подумал, что должен быть более простой способ.
Возможно, вам придется отслеживать изменение свойства в списке Colors
, а затем обновить любимые цвета вашего ученика, чтобы они соответствовали новому значению, а не старому значению. Я не думаю, что это так же просто, как привязка, если только вы не реструктурируете своих учеников FavoriteColor
, чтобы они больше походили на FavoriteColorId
, который не изменится при изменении названия цвета.
@Sinatr Я изменил его с «любимый цвет» на «название команды». Здесь вы можете понять, что изменение одного повлияет на всех остальных в этой команде, верно? Но это все же отличается от фактической смены команд.
Хорошо, давайте подумаем о «любимой команде». Итак, вы переименовываете название команды, которое нравится обоим людям. Почему в поле со списком можно переименовать название команды? Я бы ожидал, что будет какое-то отдельное место для управления командами, добавление новых или удаление в комбобоксе будет проблематичным, вам не кажется? Я бы настроил раскрывающийся список со списком, чтобы в нем был редактор команд, вместо того, чтобы создавать поле со списком IsEditable
, где можно было бы быстро изменить название команды. Такой дизайн подвержен ошибкам пользователя, случайное переименование команды вполне возможно, вам нужно подтвердить это или иметь возможность вернуться, что добавляет сложности.
string Team
это еще одна проблема. Если Teams
будет отдельной структурой данных (таблицей?), на которую ссылается каждый учащийся и с каким-то идентификатором для ссылки на конкретную команду, то изменения в команде будут автоматически влиять на всех учащихся.
Это не самое элегантное решение, но оно сработает и в целом покажет вам, как можно подойти к проблеме.
Сначала измените свой класс Student
, чтобы использовать идентификатор Team
вместо имени, и добавьте класс Team
для обработки Id
и Name
команды.
public class Student
{
public string Name { get; set; }
public int TeamId { get; set; }
public Student(string name, int teamId)
{
Name = name;
TeamId = teamId;
}
}
public class Team
{
public Team(int id, string name)
{
Id = id;
Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
}
Затем вы можете отключить эти значения в своем ComboBox
. Когда ComboBox
теряет фокус, вы можете обновить свой список Teams
с помощью события LostFocus
.
<DataGrid x:Name = "DataTable" ItemsSource = "{Binding Students}" AutoGenerateColumns = "False">
<DataGrid.Columns>
<DataGridTextColumn Binding = "{Binding Name}" Header = "Name"/>
<DataGridTemplateColumn Header = "Team Name">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource = "{Binding DataContext.Teams,
RelativeSource = {RelativeSource AncestorType=Window}}"
SelectedValue = "{Binding TeamId, UpdateSourceTrigger=LostFocus}"
SelectedValuePath = "Id"
DisplayMemberPath = "Name"
IsEditable = "True"
BorderThickness = "0"
LostFocus = "ComboBox_LostFocus"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
public partial class EditableComboBox : Window
{
public ViewModel DataModel { get; set; }
public EditableComboBox()
{
InitializeComponent();
DataModel = new ViewModel();
DataContext = DataModel;
}
private void ComboBox_LostFocus(object sender, RoutedEventArgs e)
{
var record = (e.Source as TextBox)?.DataContext as Student;
if (record == null) return;
DataModel.UpdateTeam(record.TeamId, (e.Source as TextBox)?.Text);
DataTable.Items.Refresh();
}
}
public class ViewModel
{
public IReadOnlyList<Team> Teams { get; } = new List<Team>
{
new(1, "Blue"),
new(2, "Red" ),
new(3, "Green" ),
};
public List<Student> Students { get; set; } = new List<Student>
{
new("Name 1", 1),
new("Name 2", 2),
new("Name 3", 3),
new("Name 4", 1),
new("Name 5", 2),
new("Name 6", 3),
new("Name 7", 1),
new("Name 8", 2),
new("Name 9", 3),
};
public void UpdateTeam(int id, string newName)
{
var team = Teams.FirstOrDefault(x => x.Id == id);
if (team == null) return;
team.Name = newName;
}
}
Однако это может привести к проблемам с обновлением правильной записи, когда пользователь меняет выбор ComboBox
, как отметил ОП в комментариях.
Решение этой проблемы — отделить ComboBox
от редактирования TextBox
. Это можно сделать с помощью шаблона элемента управления, но в этом примере я просто использую TextBox
рядом с ComboBox
, чтобы добиться того же эффекта.
<DataGridTemplateColumn Header = "Team Name">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width = "Auto" />
</Grid.ColumnDefinitions>
<ComboBox x:Name = "combo"
ItemsSource = "{Binding DataContext.Teams,
RelativeSource = {RelativeSource AncestorType=Window}}"
SelectedValue = "{Binding TeamId, UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath = "Id"
DisplayMemberPath = "Name"
BorderThickness = "0"
IsEditable = "True"
Width = "20"
Grid.Column = "1"/>
<TextBox x:Name = "text"
Text = "{Binding Text, Source = {x:Reference combo}}"
Padding = "2"
Margin = "0,0,0,0"
LostFocus = "ComboBox_LostFocus"
BorderThickness = "0"/>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
private void ComboBox_LostFocus(object sender, RoutedEventArgs e)
{
if (!(sender is TextBox textBlock) || !(textBlock.DataContext is Student student))
return;
DataModel.UpdateTeam(student.TeamId, textBlock.Text);
DataTable.Items.Refresh();
}
Отлично работает, за исключением одной проблемы: когда вы выбираете другую команду из раскрывающегося списка и теряете фокус, это меняет название текущей команды, а не меняет саму команду с помощью TeamId
. Добавление SelectedValue = "{Binding TeamId, UpdateSourceTrigger=LostFocus}" Я думаю, что это лучший способ исправить это (похоже, что изменение TeamId
происходит до того, как событие будет вызвано).
Хороший улов @CraigW. Я соответствующим образом обновил свой ответ.
Я не совсем понимаю, почему изменение любимого цвета Крейга с «синего» на «сине-зеленый» должно повлиять на любимый цвет Боба? Потому что вы изменили цвет, скажем, по индексу
0
, и они оба используют цвет по индексу0
? Для меня это не имеет смысла. Я ожидаю, что у каждого человека будет свой любимый цвет. Похоже на Проблема XY. Можете ли вы объяснить, чего вы хотите достичь?