2016-03-03 3 views
0

Я создал класс, полученный из DataGrid, чтобы я мог переопределить шаблоны, используемые для типов столбцов, когда DataGrid.AutoGenerateColumn установлен в True. Вот мой DataGrid класс:Значение привязки WPF не обновляется до модели

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; 
     } 
    } 
} 

Однако это вызвало DataContext нового DataGridTemplateColumn быть связан с элементом строки, так что я должен был получить еще один класс из DataGridTemplateColumn и переопределить GenerateElement() и GenerateEditingElement() функции, чтобы иметь возможность связываться содержимое шаблона возвращается к целевому свойству столбца элемента строки.

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; 
    } 
} 

А вот контроль, как представляется, на мой взгляд:

<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> 

Это похоже на работу, насколько TextBlock отображает значение свойства на элементе строки правильно. Но с CellEditingTemplate элемент управления DatePicker появляется, когда я пытаюсь отредактировать ячейку и имеет правильное начальное значение, но когда я меняю дату, изменения не сохраняются, значение источника НЕ ​​обновляется.

Почему источник не обновляется здесь?

ответ

1

Эта ошибка о двухстороннем связывании требует путь, потому что вы не можете изменить объект к другому объекту ...

В вашем случае, ваш DatePicker обязан «» который является конкретным экземпляром даты и времени. Когда DatePicker's SelectedDate изменяется, привязка не может изменить начальный экземпляр datetime на другой экземпляр datetime.

Установив «Path =.», Вы перехитрите код, который вызывает ошибку, но причина этого по-прежнему применяется.

Чтобы сделать то, что вы хотите, вам нужно будет установить Path = SomeProperty. Я предполагаю, что вы используете этот подход, потому что вы не знаете имя свойства до времени выполнения. Решением этого было бы иметь прокси-объект с известным именем свойства. Используйте это имя свойства в привязке XAML. Этот прокси должен будет синхронизировать значение свойства с реальным элементом данных.

Ниже приведена одна такая реализация, которую я только что сделал для вас. Изменение XAML к этому:

<local:DataGridEx.Resources> 
    <DataTemplate x:Key="DateTimePickerCellTemplate"> 
     <TextBlock Text="{Binding .}"/> 
    </DataTemplate> 
    <DataTemplate x:Key="DateTimePickerCellEditingTemplate"> 
     <DatePicker SelectedDate="{Binding Value}" /> 
    </DataTemplate> 
</local:DataGridEx.Resources> 

Измените класс DataGridTemplateColumnEx к этому:

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; 
    } 
} 

И ObjectProxy класс это:

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)); 
    } 
} 

Вот версия ObjectProxy что поддерживает интерфейс INotifyPropertyChanged исходного элемента:

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 
} 

Класс DataGridTemplateColumnEx должен быть обновлен для удаления ObjectProxy, если редактирование отменено или выполнено. В противном случае обработчик события PropertyChanged будет продолжать вызываться.

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); 
    } 
} 
+0

Я реализовал ваше решение, но двухсторонняя привязка не работает до сих пор, и я получаю эту ошибку в области вывода: 'System.Windows.Data Ошибка: Ошибка BindingExpression путь:: 40 свойство«Value» не найдено в 'object' '' DateTime '(HashCode = -1144325517)'. BindingExpression: Path = Value; DataItem = 'DateTime' (HashCode = -1144325517); целевым элементом является «TextBlock» (Name = ''); target свойство is 'Text' (type 'String') ' –

+0

Я не думаю, что вы внесли изменения в GenerateEditingElement. Он по-прежнему привязывается к DateTime, а не к ObjectProxy. –

+0

Извините, что вы правы. Я изменил переопределение «GenerateEditingElement», но не переопределение «GenerateElement» (для TextBlock). Теперь ошибка исчезла, но когда я изменяю значение в 'DatePicker', источник все еще не обновляется? –