Reputation: 4792
I created a class derived from DataGrid
so that I could override the templates used for column types when DataGrid.AutoGenerateColumn
is set to True
. Here is my DataGrid class:
public class DataGridEx : DataGrid
{
protected override void OnAutoGeneratingColumn(DataGridAutoGeneratingColumnEventArgs e)
{
base.OnAutoGeneratingColumn(e);
Type colDataType = e.PropertyType;
if (colDataType == typeof(DateTime))
{
// Create a new template column.
var templateColumn = new DataGridTemplateColumnEx();
templateColumn.Header = e.Column.Header;
templateColumn.CellTemplate = (DataTemplate)Resources["DateTimePickerCellTemplate"];
templateColumn.CellEditingTemplate = (DataTemplate)Resources["DateTimePickerCellEditingTemplate"];
templateColumn.SortMemberPath = e.Column.SortMemberPath;
templateColumn.ColumnName = e.PropertyName;
// Replace the auto-generated column with new template column
e.Column = templateColumn;
}
}
}
However this caused the DataContext
of the new DataGridTemplateColumn
to be bound to the row item so I had to derive another class from DataGridTemplateColumn
and override the GenerateElement()
and GenerateEditingElement()
functions to be able to bind the template contents back to the column's targeted property of the row item.
public class DataGridTemplateColumnEx : DataGridTemplateColumn
{
public string ColumnName { get; set; }
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
// The DataGridTemplateColumn uses ContentPresenter with your DataTemplate.
var cp = (ContentPresenter)base.GenerateElement(cell, dataItem);
// Reset the Binding to the specific column. The default binding is to the DataRowView.
BindingOperations.SetBinding(cp, ContentPresenter.ContentProperty, new Binding(this.ColumnName));
return cp;
}
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
// The DataGridTemplateColumn uses ContentPresenter with your DataTemplate.
var cp = (ContentPresenter)base.GenerateEditingElement(cell, dataItem);
// Reset the Binding to the specific column. The default binding is to the DataRowView.
BindingOperations.SetBinding(cp, ContentPresenter.ContentProperty, new Binding(this.ColumnName));
return cp;
}
}
And here is the control as it appears in my view:
<c:DataGridEx AutoGenerateColumns="True" ItemsSource="{Binding}">
<c:DataGridEx.Resources>
<DataTemplate x:Key="DateTimePickerCellTemplate">
<TextBlock Text="{Binding}"/>
</DataTemplate>
<DataTemplate x:Key="DateTimePickerCellEditingTemplate">
<!-- Needed to specify Path=. or else got error about two-way binding requiring a path or xpath. -->
<DatePicker Text="{Binding Path=., Mode=TwoWay}"/>
</DataTemplate>
</c:DataGridEx.Resources>
</c:DataGridEx>
This seems to work as far as the TextBlock
is displaying the value of the property on the row item correctly. But with the CellEditingTemplate
the DatePicker
control appears when I try to edit the cell and has the correct initial value but when I change the date the changes are NOT being saved, the source value is NOT being updated.
Why isn't the source being updated here?
Upvotes: 0
Views: 424
Reputation: 4322
That error about two-way binding requiring a path is because you can't change an object to another object...
In your case, your DatePicker is bound to "." which is a specific instance of a datetime. When the DatePicker's SelectedDate changes, the binding can't change the initial instance of datetime to another instance of datetime.
By setting "Path=.", you outsmart the code that throws the error but the reason for it still applies.
To do what you want, you are going to have to set Path=SomeProperty. I assume that you are using this approach because you don't know the property name until runtime. A solution to this would be to have a sort of proxy object with a known property name. Use that property name in the XAML binding. This proxy would need to sync the property's value with the real data item.
Below is one such implementation that I just made for you. Change the XAML to this:
<local:DataGridEx.Resources>
<DataTemplate x:Key="DateTimePickerCellTemplate">
<TextBlock Text="{Binding .}"/>
</DataTemplate>
<DataTemplate x:Key="DateTimePickerCellEditingTemplate">
<DatePicker SelectedDate="{Binding Value}" />
</DataTemplate>
</local:DataGridEx.Resources>
Change the DataGridTemplateColumnEx class to this:
class DataGridTemplateColumnEx : DataGridTemplateColumn
{
public string ColumnName { get; set; }
protected override System.Windows.FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
// The DataGridTemplateColumn uses ContentPresenter with your DataTemplate.
var cp = (ContentPresenter)base.GenerateElement(cell, dataItem);
// Reset the Binding to the specific column. The default binding is to the DataRowView.
BindingOperations.SetBinding(cp, ContentPresenter.ContentProperty, new Binding(this.ColumnName));
return cp;
}
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
// Create our ObjectProxy that will update our dataItem's ColumnName property
var op = new ObjectProxy(dataItem, ColumnName);
// Generate the editing element using our ObjectProxy
var cp = (ContentPresenter)base.GenerateEditingElement(cell, op);
// Reset the Binding to our ObjectProxy
BindingOperations.SetBinding(cp, ContentPresenter.ContentProperty, new Binding(".") { Source = op });
return cp;
}
}
And the ObjectProxy class is this:
public class ObjectProxy : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private object dataItem;
private System.Reflection.PropertyInfo prop;
private object val;
public object Value
{
get { return val; }
set
{
if (val != value)
{
val = value;
OnPropertyChanged("Value");
}
}
}
public ObjectProxy(object DataItem, string propertyName)
{
this.dataItem = DataItem;
if (dataItem != null)
{
prop = dataItem.GetType().GetProperty(propertyName);
if (prop != null)
{
val = prop.GetValue(dataItem);
}
}
}
private void OnPropertyChanged(string name)
{
if (prop != null)
prop.SetValue(dataItem, val);
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
Here is a version of the ObjectProxy that supports the source item's INotifyPropertyChanged interface:
public class ObjectProxy : INotifyPropertyChanged, IDisposable
{
public event PropertyChangedEventHandler PropertyChanged;
private object dataItem;
private System.Reflection.PropertyInfo prop;
private object val;
public object Value
{
get { return val; }
set
{
if (!Object.Equals(val, value))
{
val = value;
OnPropertyChanged("Value");
}
}
}
public ObjectProxy(object DataItem, string propertyName)
{
this.dataItem = DataItem;
if (dataItem != null)
{
prop = dataItem.GetType().GetProperty(propertyName);
if (prop != null)
{
val = prop.GetValue(dataItem);
// Sync from dataItem to ObjectProxy
if (dataItem is INotifyPropertyChanged)
{
INotifyPropertyChanged pc = dataItem as INotifyPropertyChanged;
pc.PropertyChanged += DataItemPropertyChanged;
}
}
}
}
private void OnPropertyChanged(string name)
{
if (prop != null)
prop.SetValue(dataItem, val);
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
// The source item changed - Update our Value
private void DataItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (prop != null && e.PropertyName == prop.Name)
{
Value = prop.GetValue(dataItem);
}
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
if (dataItem is INotifyPropertyChanged)
{
var pc = dataItem as INotifyPropertyChanged;
pc.PropertyChanged -= DataItemPropertyChanged;
}
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
}
#endregion
}
The DataGridTemplateColumnEx class would need to be updated to Dispose of the ObjectProxy when editing is canceled or committed. Otherwise, the PropertyChanged event handler will continue to be called.
class DataGridTemplateColumnEx : DataGridTemplateColumn
{
public string ColumnName { get; set; }
protected override System.Windows.FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
// The DataGridTemplateColumn uses ContentPresenter with your DataTemplate.
var cp = (ContentPresenter)base.GenerateElement(cell, dataItem);
// Reset the Binding to the specific column. The default binding is to the DataRowView.
BindingOperations.SetBinding(cp, ContentPresenter.ContentProperty, new Binding(this.ColumnName));
return cp;
}
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
// Create our ObjectProxy that will update our dataItem's ColumnName property
var op = new ObjectProxy(dataItem, ColumnName);
// Generate the editing element using our ObjectProxy
var cp = (ContentPresenter)base.GenerateEditingElement(cell, op);
// Reset the Binding to our ObjectProxy
BindingOperations.SetBinding(cp, ContentPresenter.ContentProperty, new Binding(".") { Source = op });
return cp;
}
private void DisposeOfProxyObject(FrameworkElement editingElement)
{
var cp = editingElement as ContentPresenter;
if (cp != null)
{
var op = cp.Content as ObjectProxy;
if (op != null)
op.Dispose();
}
}
protected override void CancelCellEdit(FrameworkElement editingElement, object uneditedValue)
{
DisposeOfProxyObject(editingElement);
base.CancelCellEdit(editingElement, uneditedValue);
}
protected override bool CommitCellEdit(FrameworkElement editingElement)
{
DisposeOfProxyObject(editingElement);
return base.CommitCellEdit(editingElement);
}
}
Upvotes: 1