Двусторонняя привязка 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 недвижимость без конвертера.

Другие вопросы по тегам