I am creating a DataGridRadioButtonColumn
for my WPF project. Here is what it looks like:
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;
}
}
And here is an example of its use:
<local:DataGridRadioButtonColumn
Width="0.33*"
Binding="{Binding PrimaryChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Group="Group1"
Header="PRI" />
Everything works as I expect it to, including that clicking on a radio button "unchecks" the one that was originally selected. This unchecking works by virtue of the Group dependency property.
The only problem I am having is that the unchecked radio button does not register as a row edit in the grid. Only the radio button that I clicked registers a row edit, and in order for the data to be saved correctly both rows (the one containing the radio button that I clicked, and the one containing the radio button that was unchecked) must be saved.
How do I tell the data row whose radio button was unchecked that it was edited, so that it properly updates the matching tuple in the DataGrid's bound collection?
Note that I implement the IEditableObject
interface in the Model that's used in each row, so it's not as simple as just relying on INotifyPropertyChanged
. It really needs to trigger a BeginEdit()
in the DataGrid Row. I don't think that the programmatic clearing of the radio button fires PropertyChanged
anyway, because the change is not reflected in the underlying Model object.
As requested, here is an MCVE (or the better part of one, anyway):
<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>
<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;
}
}
}
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; }
}
}
As you can see, this code is unremarkable.
Here is where it gets interesting. Let's place an IEditableObject
implementation on the Model. IEditableObject
is recognized by the DataGrid; it allows you to provide things like change tracking and undo capability for each data row:
public class Model : EditableValidatableObject<Model>
{
public string Name { get; set; }
public bool? PrimaryChecked
{
get;
set;
}
}
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
is unremarkable; it's just an implementation of INotifyDataErrorInfo
that uses data annotations for validation.
The critical part of the IEditableObject
implementation above is the BeginEdit()
method, which the data grid row uses to signal the underlying model that an edit has occurred. This method gets called when a Radio Button is clicked, but not when the other radio button is automatically unchecked.
Since BeginEdit()
never gets called on the unchecked row, the Edited property never gets set. I rely on the Edited property to know which records I need to save back to the database.
After giving it some thought, I've decided to make some changes to my DataGridRadioButtonColumn
implementation. It now looks like this:
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;
}
}
Here's how it works. I added a dictionary that captures the model for each of the Radio Buttons, and when a Radio Button is created, I add the model to the new dictionary and hook the Unchecked
event on the Radio button:
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;
}
Then, I just set the Edited
property in the Model when the event fires:
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 { }
}