Как сделать WPF DataGridColumn связанным, редактируемым ComboBox, в котором изменения элементов распространяются на другие строки?

Я использую 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, но оно оказалось настолько сложным, что я подумал, что должен быть более простой способ.

Я не совсем понимаю, почему изменение любимого цвета Крейга с «синего» на «сине-зеленый» должно повлиять на любимый цвет Боба? Потому что вы изменили цвет, скажем, по индексу 0, и они оба используют цвет по индексу 0? Для меня это не имеет смысла. Я ожидаю, что у каждого человека будет свой любимый цвет. Похоже на Проблема XY. Можете ли вы объяснить, чего вы хотите достичь?

Sinatr 20.08.2024 10:45

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

bigcrazyal 20.08.2024 14:50

@Sinatr Я изменил его с «любимый цвет» на «название команды». Здесь вы можете понять, что изменение одного повлияет на всех остальных в этой команде, верно? Но это все же отличается от фактической смены команд.

Craig W 20.08.2024 16:23

Хорошо, давайте подумаем о «любимой команде». Итак, вы переименовываете название команды, которое нравится обоим людям. Почему в поле со списком можно переименовать название команды? Я бы ожидал, что будет какое-то отдельное место для управления командами, добавление новых или удаление в комбобоксе будет проблематичным, вам не кажется? Я бы настроил раскрывающийся список со списком, чтобы в нем был редактор команд, вместо того, чтобы создавать поле со списком IsEditable, где можно было бы быстро изменить название команды. Такой дизайн подвержен ошибкам пользователя, случайное переименование команды вполне возможно, вам нужно подтвердить это или иметь возможность вернуться, что добавляет сложности.

Sinatr 20.08.2024 16:42
string Team это еще одна проблема. Если Teams будет отдельной структурой данных (таблицей?), на которую ссылается каждый учащийся и с каким-то идентификатором для ссылки на конкретную команду, то изменения в команде будут автоматически влиять на всех учащихся.
Sinatr 20.08.2024 16:51
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
5
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Это не самое элегантное решение, но оно сработает и в целом покажет вам, как можно подойти к проблеме.

Сначала измените свой класс 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.

XAML

<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 происходит до того, как событие будет вызвано).

Craig W 21.08.2024 01:23

Хороший улов @CraigW. Я соответствующим образом обновил свой ответ.

egeer 21.08.2024 01:29

Другие вопросы по теме