Двусторонняя привязка DataGrid со структурой записи (динамические столбцы и строки)
У меня есть структура записи, которую я пытаюсь связать с DataGrid
, По сути, мои столбцы динамически определяются моделью представления. Значение, которое должно отображаться (и редактироваться) в ячейке сетки, должно извлекаться с использованием функции для каждой записи (модель представления за каждой строкой сетки).
Я посмотрел на этот вопрос, и он получил меня на полпути туда. К сожалению, я не могу использовать DynamicObject
потому что мои свойства в записи являются именами в пространстве имен и поэтому не могут быть представлены строкой. Я попробовал вариант, в котором я преобразовал это имя в пространстве имен в привязываемую строку (заменив все недопустимые символы подчеркиванием), но похоже, что DynamicObject
назвал бы TryGetMember
каждый раз, когда я перемещался DataGrid
(т.е. он будет вызывать функцию для каждой ячейки каждый раз, когда я прокручиваю). Эта производительность не будет достаточно хорошей.
Это то, что у меня пока что работает для отображения данных (т.е. GetValue
):
public interface IDataSet
{
IEnumerable<IRecord> Records { get; }
IEnumerable<IProperty> PropertiesToDisplay { get; }
}
public interface IProperty
{
XName Name { get; }
}
public interface IRecord
{
IEnumerable<KeyValuePair<IProperty, object>> Values { get; set; }
object GetValue(IProperty property);
void SetValue(IProperty property, object value);
}
internal class SomeClass
{
DataGrid myGrid; // this is defined in the XAML
IDataSet viewModel; // this is the DataContext
private void BindToViewModelUsingConverter()
{
foreach (IProperty property in viewModel.PropertiesToDisplay)
{
Binding binding = new Binding();
binding.Converter = new ConvertMyDataConverter();
binding.ConverterParameter = property;
var column = new DataGridTextColumn
{
Header = property.Name.LocalName,
Binding = binding
};
myGrid.Columns.Add(column);
}
myGrid.ItemsSource = viewModel.Records;
}
}
internal class ConvertMyDataConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var record = value as IRecord;
var property = parameter as IProperty;
if (record != null && property != null)
{
return record.GetValue(property);
}
return DependencyProperty.UnsetValue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
// not sure what to do here, yet
throw new NotImplementedException();
}
}
Приведенный выше код работает, когда дело доходит до отображения значений. Однако, как только я пытаюсь редактировать значения, я получаю исключение:
**Two-way binding requires Path or XPath**
Это имеет смысл, потому что даже в моем конвертере выше, я не уверен, что я бы написал в ConvertBack
функция, так как я хочу, чтобы источник остался прежним (так как я привязан к IRecord
и уже назвали IRecord.SetValue
функция для обновления данных.) Однако, похоже, ConvertBack
не дал бы мне ссылку на IRecord
даже если бы я мог сделать двустороннее связывание без пути.
Есть что-то тривиальное, что мне не хватает? Я не против разработки пользовательского элемента управления, но хотел бы несколько советов / идей о том, как подходить к проблеме.
1 ответ
Я решил это используя ICustomTypeDescriptor
а также ITypedList
, Вместо Records
быть IEnumerable<IRecord>
, Я сделал это пользовательскую коллекцию, которая реализовала IList<IRecord>
так же как ITypedList
, Для ITypedList
реализации, я вернул дескрипторы свойств, которые обернуты IProperty
реализация:
public interface IDataSet
{
TypedRecordsCollection Records { get; }
IEnumerable<IProperty> PropertiesToDisplay { get; }
}
public class TypedRecordsCollection : ObservableCollection<RecordViewModel>, ITypedList
{
private PropertyDescriptorCollection _properties;
public TypedRecordsCollection(IEnumerable<IRecord> records)
{
_properties = new PropertyDescriptorCollection(
PropertiesToDisplay
.Select(prop => new CustomPropertyDescriptor(prop))
.ToArray());
records.ForEach(rec => Items.Add(new RecordViewModel(rec, _properties)));
}
public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
{
return _properties;
}
}
public interface IRecordViewModel : ICustomTypeDescriptor
{
object GetCellValue(IProperty property);
void SetCellValue(IProperty property, object value);
}
public class RecordViewModel : CustomTypeDescriptor, IRecord
{
private IRecord _record;
private PropertyDescriptorCollection _properties;
public RecordViewModel(IRecord record, PropertyDescriptorCollection properties)
{
_record = record;
_properties = properties;
}
public object GetCellValue(IProperty property)
{
return _record.GetValue(property);
}
public void SetCellValue(IProperty property, object value)
{
_record.SetValue(property, value);
}
public override PropertyDescriptorCollection GetProperties()
{
return _properties;
}
}
public class CustomPropertyDescriptor : PropertyDescriptor
{
private IProperty _property;
public CustomPropertyDescriptor(IProperty property)
: base(GetCleanName(property.Name), new Attribute[0])
{
_property = property;
}
public override object GetValue(object component)
{
var recordViewModel = component as IRecordViewModel;
if (recordViewModel != null)
{
return recordViewModel.GetCellValue(_property);
}
return null;
}
public override void SetValue(object component, object value)
{
var recordViewModel = component as IRecordViewModel;
if (recordViewModel != null)
{
recordViewModel.SetCellValue(_property, value);
}
}
private static string GetCleanName(XName name)
{
// need to return a XAML bindable string.. aka no "{}" or ".", etc.
}
}
Теперь в моем XAML я могу просто установить AutogenerateColumns
к истине и привязать непосредственно к Records
недвижимость без конвертера.