Как сообщить строке в WPF DataGrid, что одна из ее ячеек была программно изменена? - PullRequest
1 голос
/ 25 апреля 2019

Я создаю DataGridRadioButtonColumn для своего проекта WPF.Вот как это выглядит:

public class DataGridRadioButtonColumn : DataGridBoundColumn
{
    private Dictionary<DataGridCell, RadioButton> _buttons = new Dictionary<DataGridCell, RadioButton>();

    public string Group { get; set; }

    public static readonly DependencyProperty GroupProperty = RadioButton.GroupNameProperty.AddOwner(
        typeof(DataGridRadioButtonColumn), new FrameworkPropertyMetadata("DefaultGroup"));

    protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
    {
        // Only generate the buttons once, to preserve the Group.
        if (_buttons.ContainsKey(cell))
        {
            return (_buttons[cell]);
        }
        var radioButton = new RadioButton { GroupName = Group };
        BindingOperations.SetBinding(radioButton, ToggleButton.IsCheckedProperty, Binding);
        _buttons.Add(cell, radioButton);
        return radioButton;
    }

    protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
    {
       // Use the same one we generated before.
       return _buttons[cell];
    }

    protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs)
    {
        var radioButton = editingElement as RadioButton;
        if (radioButton == null) return null;
        return radioButton.IsChecked;
    }
}

А вот пример его использования:

<local:DataGridRadioButtonColumn
    Width="0.33*"
    Binding="{Binding PrimaryChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
    Group="Group1"
    Header="PRI" />

Все работает так, как я ожидаю, включая нажатие кнопки-переключателя "снимает флажок "тот, который был первоначально выбран.Это снятие флажка работает благодаря свойству зависимости группы.

Единственная проблема, с которой я сталкиваюсь, состоит в том, что неконтролируемый переключатель не регистрируется как редактирование строки в сетке.Только переключатель, по которому я нажал, регистрирует редактирование строки, и для правильного сохранения данных обе строки (тот, который содержит переключатель, который я нажал, и тот, который был переключатель, который былне проверено) должно быть сохранено.

Как сообщить строке данных, радиокнопка которой не была отмечена, что она была отредактирована, чтобы она должным образом обновляла соответствующий кортеж в связанной коллекции DataGrid?

Примечаниечтобы я реализовал интерфейс IEditableObject в модели, используемой в каждой строке, так что это не так просто, как полагаться на INotifyPropertyChanged.Это действительно должно вызвать BeginEdit() в строке DataGrid.Я не думаю, что программная очистка переключателя в любом случае запускает PropertyChanged, потому что изменение не отражается в базовом объекте Model.


В соответствии с запросом, здесь MCVE (илив любом случае, лучше):

APP.XAML

<Application x:Class="WpfApp11.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WpfApp11"
             StartupUri="MainWindow.xaml">
    <Application.Resources>

    </Application.Resources>
</Application>

MainWindow.XAML

<Window
    x:Class="WpfApp11.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:local="clr-namespace:WpfApp11"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Grid>
        <DataGrid ItemsSource="{Binding Items, UpdateSourceTrigger=PropertyChanged}">
            <DataGrid.Columns>
                <DataGridTextColumn
                    Width="0.7*"
                    Binding="{Binding Path=Name, Mode=TwoWay}"
                    Header="Name" />
                <local:DataGridRadioButtonColumn
                    Width="0.3*"
                    Binding="{Binding PrimaryChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                    Group="Group1"
                    Header="PRI" />

            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

Модель

using PropertyChanged; //Fody

namespace WpfApp11
{
    [AddINotifyPropertyChangedInterface]  // PropertyChanged.Fody
    public class Model
    {
        public string Name { get; set; }
        public bool? PrimaryChecked
        {
            get;
            set;
        }
    }
}

ViewModel

using System.Collections.ObjectModel;
using PropertyChanged; // Fody

namespace WpfApp11
{
    [AddINotifyPropertyChangedInterface] // PropertyChanged.Fody
    public class ViewModel
    {
        public ViewModel()
        {
            Items = new ObservableCollection<Model>
            {
                new Model {Name = "George"},
                new Model {Name = "Fred"},
                new Model {Name = "Tom"},
            };
        }
        public ObservableCollection<Model> Items { get; set; }
    }
}

Как видите, этот код ничем не примечателен.

Вот где это становится интересным.Давайте поместим реализацию IEditableObject в модель.IEditableObject распознается DataGrid;это позволяет вам предоставлять такие вещи, как отслеживание изменений и возможность отмены для каждой строки данных:

public class Model : EditableValidatableObject<Model>
{
    public string Name { get; set; }
    public bool? PrimaryChecked
    {
        get;
        set;
    }
}

EditableValidatableObject.cs

using PropertyChanged;
using System;
using System.ComponentModel;

namespace WpfApp11
{
    /// <summary>
    /// Provides an implementation of the IEditableObject and INotifyDataErrorInfo interfaces for data transfer objects.
    /// </summary><remarks>
    /// The IEditableObject interface is typically used to capture the BeginEdit, EndEdit, and CancelEdit semantics of a DataRowView.
    /// Making something an IEditableObject enables full editing and undo capabilities in a DataGrid.
    /// 
    /// The INotifyDataErrorInfo implementation uses Validation Attributes to validate the values of properties on the DTO.
    /// This information is used to indicate that a value entered by the user is invalid.
    /// 
    /// See T_Asset.cs and T_TestPoint.cs for usage examples.  
    /// </remarks>
    [AddINotifyPropertyChangedInterface]
    public abstract class EditableValidatableObject<T> : AnnotationValidationViewModel, IEditableObject
    {
        /// <summary>
        /// Constructor, sets up the INotifyDataErrorInfo implementation.
        /// </summary>
        private T Cache { get; set; }

        private object CurrentModel { get { return this; } }

        public RelayCommand CancelEditCommand
        {
            get { return new RelayCommand(CancelEdit); }
        }

        private bool IsDirty
        {
            get
            {
                if (Cache == null) return false;
                foreach (var info in CurrentModel.GetType().GetProperties())
                {
                    if (!info.CanRead || !info.CanWrite)
                        continue;

                    var oldValue = info.GetValue(Cache, null);
                    var currentValue = info.GetValue(CurrentModel, null);

                    if (oldValue == null && currentValue != null)
                        return true;

                    if (oldValue != null && !oldValue.Equals(currentValue))
                        return true;
                }
                return false;
            }
        }

        #region IEditableObject Implementation
        public bool Added { get; set; }
        public bool Edited { get; set; }
        public bool Deleted { get; set; }

        public void BeginEdit()
        {
            Cache = Activator.CreateInstance<T>();
            var type = CurrentModel.GetType();

            //Set Properties of Cache
            foreach (var info in type.GetProperties())
            {
                if (!info.CanRead || !info.CanWrite) continue;
                var oldValue = info.GetValue(CurrentModel, null);
                Cache.GetType().GetProperty(info.Name).SetValue(Cache, oldValue, null);
            }

            if (!Added && !Deleted && IsDirty)
            {
                Edited = true;
            }
        }

        public virtual void EndEdit()
        {
            if (!Added && !Deleted && IsDirty)
            {
                Edited = true;
            }
            Cache = default(T);
        }

        public void CancelEdit()
        {
            if (Cache == null) return;

            foreach (var info in CurrentModel.GetType().GetProperties())
            {
                if (!info.CanRead || !info.CanWrite) continue;
                var oldValue = info.GetValue(Cache, null);
                CurrentModel.GetType().GetProperty(info.Name).SetValue(CurrentModel, oldValue, null);
            }
        }
        #endregion
    }
}

AnnotationValidationViewModel - не имеет значения;это просто реализация INotifyDataErrorInfo, которая использует аннотации данных для проверки.

Важнейшей частью реализации IEditableObject, описанной выше, является метод BeginEdit(), который строка сетки данных использует для сигнализации базовой модели, котораяредактирование произошло.Этот метод вызывается при нажатии кнопки-переключателя, но , когда другая кнопка-переключатель автоматически не проверяется.

Поскольку BeginEdit() никогда не вызывается в строке без проверки, свойство Edited никогда не изменяетсяустанавливаетсяЯ полагаюсь на свойство Edited, чтобы узнать, какие записи мне нужно сохранить обратно в базу данных.

1 Ответ

0 голосов
/ 25 апреля 2019

Подумав немного, я решил внести некоторые изменения в мою DataGridRadioButtonColumn реализацию. Теперь это выглядит так:

public class DataGridRadioButtonColumn : DataGridBoundColumn
{
    private Dictionary<DataGridCell, RadioButton> _buttons = new Dictionary<DataGridCell, RadioButton>();
    private Dictionary<RadioButton, dynamic> _models = new Dictionary<RadioButton, dynamic>();

    public string Group { get; set; }

    public static readonly DependencyProperty GroupProperty = RadioButton.GroupNameProperty.AddOwner(
        typeof(DataGridRadioButtonColumn), new FrameworkPropertyMetadata("Group1"));

    protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
    {
        if (_buttons.ContainsKey(cell))
        {
            return (_buttons[cell]);
        }
        var radioButton = new RadioButton { GroupName = Group };
        radioButton.Unchecked += RadioButton_Unchecked;

        BindingOperations.SetBinding(radioButton, ToggleButton.IsCheckedProperty, Binding);
        _buttons.Add(cell, radioButton);
        _models.Add(radioButton, dataItem);
        return radioButton;
    }

    private void RadioButton_Unchecked(object sender, RoutedEventArgs e)
    {
        var button = sender as RadioButton;
        dynamic model = _models[button];
        try
        {
            model.Edited = true;
        }
        catch { }
    }

    protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
    {
       return _buttons[cell];
    }

    protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs)
    {
        var radioButton = editingElement as RadioButton;
        if (radioButton == null) return null;
        return radioButton.IsChecked;
    }
}

Вот как это работает. Я добавил словарь, который фиксирует модель для каждой из радиокнопок, и когда радиокнопка создается, я добавляю модель в новый словарь и подключаю событие Unchecked к радиокнопке:

    protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
    {
        if (_buttons.ContainsKey(cell))
        {
            return (_buttons[cell]);
        }
        var radioButton = new RadioButton { GroupName = Group };
        radioButton.Unchecked += RadioButton_Unchecked; // Added

        BindingOperations.SetBinding(radioButton, ToggleButton.IsCheckedProperty, Binding);
        _buttons.Add(cell, radioButton);
        _models.Add(radioButton, dataItem); // Added
        return radioButton;
    }

Затем я просто устанавливаю свойство Edited в модели при возникновении события:

private void RadioButton_Unchecked(object sender, RoutedEventArgs e)
{
    var button = sender as RadioButton;
    dynamic model = _models[button];
    try
    {
        // Notify IEditableObject implementation, if it exists.
        model.Edited = true;
    }
    catch { }
}
...