WPF DataGrid: как привязать объект для отражения элемента, строка которого проверяется


У меня есть datagrid, заполненная элементами, и флажок для каждого элемента. Я ищу способ, чтобы объект в моем ViewModel был тем элементом, у которого в данный момент установлен флажок.
Вот мой XAML до сих пор:

<Window x:Class="fun_with_DataGridCheckBoxColumns.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:fun_with_DataGridCheckBoxColumns"
    mc:Ignorable="d"
    Title="MainWindow" Height="350" Width="525">
<DockPanel>
    <StackPanel Orientation="Horizontal" DockPanel.Dock="Top">
        <Label Content="Chosen One : " />
        <Label Content="{Binding ChosenOne.Name, Mode=OneWay}" />
    </StackPanel>
    <DataGrid ItemsSource="{Binding People}" AutoGenerateColumns="False" CanUserAddRows="False">
        <DataGrid.Columns>
            <DataGridTextColumn Header="ID" Binding="{Binding ID, Mode=OneWay}" IsReadOnly="True"/>
            <DataGridTextColumn Header="Name" Binding="{Binding Name, Mode=OneWay}" IsReadOnly="True"/>
            <DataGridCheckBoxColumn Header="Is Chosen"/>
        </DataGrid.Columns>
    </DataGrid>
</DockPanel>

И мой CS:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;

namespace fun_with_DataGridCheckBoxColumns
{
    public partial class MainWindow : Window
    {
        public Person ChosenOne { get; set; }

        public MainWindow()
        {
        InitializeComponent();
        DataContext = new Viewmodel();
        }
    }

    public class Viewmodel : INotifyPropertyChanged
    {
        public ObservableCollection<Person> People { get; private set; }
        private Person _chosenOne = null;
        public Person ChosenOne
        {
            get
            {
                if (_chosenOne == null) { return new Person { Name = "Does Not Exist" }; }
                else return _chosenOne;
            }
            set
            {
                _chosenOne = value;
                NotifyPropertyChanged("ChosenOne");
            }
        }

        public Viewmodel()
        {
            People = new ObservableCollection<Person>
            {
                new Person { Name = "John" },
                new Person { Name = "Marie" },
                new Person { Name = "Bob" },
                new Person { Name = "Sarah" }
            };
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public void NotifyPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public class Person
    {
        private static int person_quantity = 0;
        private int _id = ++person_quantity;
        public int ID { get { return _id; } }
        public string Name { get; set; }
    }
}

Вот поведение, которое я ищу:

  • ChosenOne в ViewModel становится тем человеком, у которого установлен флажок
  • когда флажок установлен, все остальные unchecked
  • если флажки не установлены, устанавливает ChosenOne в null

В основном это то же самое поведение, как если бы я поместил это в DataGrid (XAML):

SelectedItem="{Связывание ChosenOne, Mode=TwoWay} "

Но в моем случае ChosenOne не может быть SelectedItem datagrid, так как мне нужно SelectedItem для чего-то другого, и я должен использовать флажки по причинам компании.

Я не нашел, как имитировать эту логику" SelectedItem". с флажками.

Я знаю, что могу поместить свойство "bool IsChosen" в свой класс Person и привязать к нему флажок, но я бы предпочел этого избежать. Это будет моим решением, если все остальное потерпит неудачу.

Спасибо.

2 2

2 ответа:

Альтернативой было бы обернуть ваш объект чем-то, что поддерживает проверку. Источник

using System.ComponentModel;

namespace Jarloo
{
    public class CheckedListItem<T> : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private bool isChecked;
        private T item;

        public CheckedListItem()
        {}

        public CheckedListItem(T item, bool isChecked=false)
        {
            this.item = item;
            this.isChecked = isChecked;
        }

        public T Item
        {
            get { return item; }
            set
            {
                item = value;
                if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Item"));
            }
        }


        public bool IsChecked
        {
            get { return isChecked; }
            set
            {
                isChecked = value;
                if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("IsChecked"));
            }
        }
    }
}

Добавьте свойство IsChecked в класс Person и реализуйте интерфейс INotifyPropertyChanged:

public class Person : INotifyPropertyChanged
{
    private static int person_quantity = 0;
    private int _id = ++person_quantity;
    public int ID { get { return _id; } }
    public string Name { get; set; }

    private bool _isChecked;
    public bool IsChecked
    {
        get { return _isChecked; }
        set { _isChecked = value; NotifyPropertyChanged("IsChecked"); }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void NotifyPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Свяжите столбец DataGridCheckBoxColumn с этим свойством:

<DataGridCheckBoxColumn Header="Is Chosen" Binding="{Binding IsChecked, UpdateSourceTrigger=PropertyChanged}"/>

Затем можно обработать логику в классе модели представления. Это должно гарантировать, что только один Person выбран одновременно:

public class Viewmodel : INotifyPropertyChanged
{
    public ObservableCollection<Person> People { get; private set; }
    private Person _chosenOne = null;
    public Person ChosenOne
    {
        get
        {
            if (_chosenOne == null) { return new Person { Name = "Does Not Exist" }; }
            else return _chosenOne;
        }
        set
        {
            _chosenOne = value;
            NotifyPropertyChanged("ChosenOne");
        }
    }

    public Viewmodel()
    {
        People = new ObservableCollection<Person>
        {
            new Person { Name = "John" },
            new Person { Name = "Marie" },
            new Person { Name = "Bob" },
            new Person { Name = "Sarah" }
        };

        foreach(Person p in People)
            p.PropertyChanged += P_PropertyChanged;
    }

    private bool handle = true;
    private void P_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if(handle && e.PropertyName == "IsChecked")
        {
            handle = false;
            //uncheck all other persons
            foreach (Person p in People)
                if(p != sender)
                    p.IsChecked = false;

            ChosenOne = sender as Person;

            handle = true;
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void NotifyPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Если вы планируете добавлять новые объекты Person в коллекцию People динамически во время выполнения, вы также должны убедиться, что вы обрабатываете событие PropertyChanged для них:

public Viewmodel()
{
    People = new ObservableCollection<Person>
            {
                new Person { Name = "John" },
                new Person { Name = "Marie" },
                new Person { Name = "Bob" },
                new Person { Name = "Sarah" }
            };

    foreach (Person p in People)
        p.PropertyChanged += P_PropertyChanged;


    People.CollectionChanged += (s, e) =>
    {
        if (e.NewItems != null)
        {
            foreach (object person in e.NewItems)
            {
                (person as INotifyPropertyChanged).PropertyChanged
                    += new PropertyChangedEventHandler(P_PropertyChanged);
            }
        }

        if (e.OldItems != null)
        {
            foreach (object person in e.OldItems)
            {
                (person as INotifyPropertyChanged).PropertyChanged
                    -= new PropertyChangedEventHandler(P_PropertyChanged);
            }
        }
    };

    People.Add(new Person() { Name = "New..." });
}