如何在WPF DataGrid中编辑数组

时间:2019-08-23 01:31:56

标签: c# wpf datagrid

我正在研究WPF程序,用于编辑专有脚本类。该模型由以下类组成:

public MethodCall
{
    public Dictionary<string, object> Parameters { get; }

    public MethodCall()
    {
        Parameters = new Dictionary<string, object>();
    }

    public MethodCall(Dictionary<string, object> parameters)
    {
        Parameters = parameters ?? throw new ArgumentNullException(nameof(parameters);
    }

    public void Execute(Client client)
    {
        // Does stuff
    }
}

public class Script
{
    public List<Request> Requests { get; }

    public void Execute(Client client)
    {
        foreach (var request in Requests)
        {
            request.Execute(client);
        }
    }
}

要显示包含Script请求的CallMethod,我具有以下View Model类。

public class ScriptViewModel : INotifyPropertyChanged
{
    public Script Script { get; set; }  // Setter raises PropertyChanged

    public ObservableCollection<RequestViewModel> { get; }
}

public class RequestViewModel : INotifyPropertyChanged
{
    public Request Request { get; set; } // Setter raises PropertyChanged

    public ObservableCollection<ParameterViewModel> Parameters { get; }
}

public class ParameterViewModel : INotifyPropertyChanged
{
    public bool IsDirty { get; set; } // Raises PropertyChanged
    public string Name { get; set; } // Raises PropertyChanged
    public object Value { get; set; } // Raises PropertyChanged
}

当前,我正在使用DataGrid控件来显示和编辑Parameters集合。但是,由于Value属性是一个对象,因此它可以是任何对象,包括对象数组。我的问题是如何在WPF应用程序中编辑此数据。我正在使用DataGridTextColumn来显示Name属性,并且工作正常。当前,我还使用DataGridTextColumn来显示Value属性,但是效果不是很好。

我想我想使用DataGridTemplateColumn' for theproperty, but I'd need to use different templates depending upon what the particular type is. If the type isn't an array, I'd use a TextBox, but if it is an Array, I'd probably use another DataGrid`。

如何编写XAML?

1 个答案:

答案 0 :(得分:0)

考虑到这个问题有多小的吸引力,我最终提出了自己的解决方案。我在这里分享它,以防其他人在类似情况下可以使用它。

我首先要做的是在App.xaml中定义许多不同的DataTemplates

    <DataTemplate x:Key="ObjectArrayTemplate"
                  DataType="system:Array">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>

            <DataGrid x:Name="ParameterItemsGrid"
                      Grid.Column="0"
                      AutoGenerateColumns="False"
                      CanUserAddRows="False"
                      CanUserDeleteRows="False"
                      CanUserReorderColumns="False"
                      CanUserResizeRows="False"
                      CanUserResizeColumns="False"
                      CanUserSortColumns="False"
                      Initialized="OnArrayDataGridInitialized">
                <DataGrid.Columns>
                    <DataGridTemplateColumn CellTemplateSelector="{StaticResource ValueDataSelector}"
                                            Width="*" />
                    <DataGridTemplateColumn Width="52">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <StackPanel>
                                    <Button Command="local:EditorCommands.DeleteItem"
                                            CommandParameter="{Binding Path=Item, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type DataGridRow}}}"
                                            Content="Delete"
                                            Height="25"
                                            Margin="5"
                                            VerticalAlignment="Top" />
                                </StackPanel>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                </DataGrid.Columns>
            </DataGrid>

            <StackPanel Grid.Column="1">
                <Button Content="Add Item"
                        Command="local:EditorCommands.AddItem"
                        Margin="5" />
            </StackPanel>
        </Grid>
    </DataTemplate>

    <DataTemplate x:Key="ValueArrayTemplate"
                  DataType="{x:Type system:Array}">
        <DataGrid Initialized="OnArrayDataGridInitialized">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Item" />
            </DataGrid.Columns>
        </DataGrid>
    </DataTemplate>

    <DataTemplate x:Key="ValueTemplate"
                  DataType="system:Object">
        <TextBox Initialized="OnValueTextBoxInitialized" />
    </DataTemplate>

第一个DataTemplate用于显示类型为程序中定义的对象数组的参数。第二个用于显示简单类型的对象数组(int,字符串等)。第三个模板用于显示类型为简单类型的参数或值为简单类型的项目。

我不会显示针对不同程序特定类的其他模板。关于这些的重要一点是,我在所有这些类上都实现了INotifyPropertyChanged,并且在绑定到UpdateSourceTrigger时必须设置PropertyChanged。我做了所有这些工作,因此绑定将是2种方式并起作用。

在2 Initialized和单个简单类型DataGrids中定义的TextBox事件处理程序中可以找到实现此目的的技巧:

    private void OnArrayDataGridInitialized(object sender, EventArgs e)
    {
        var valueGrid = (DataGrid)sender;
        var parameterVm = (ParameterViewModel)valueGrid.DataContext;

        // Bind the value grid's ItemsSource property to the ParameterViewModel's Items property.
        var itemsBinding = new Binding("Items")
        {
            Source = parameterVm,
            ValidatesOnDataErrors = true
        };
        valueGrid.SetBinding(DataGrid.ItemsSourceProperty, itemsBinding);

        // Bind the value grid's SelectedItem property to the ParameterViewModel's CurrentItem property.
        var currentItemBinding = new Binding("CurrentItem")
        {
            Mode = BindingMode.TwoWay,
            Source = parameterVm,
            UpdateSourceTrigger = UpdateSourceTrigger.LostFocus
        };
        valueGrid.SetBinding(DataGrid.SelectedItemProperty, currentItemBinding);

        // Bind the value grid's SelectedIndex property to the ParameterViewModel's CurrentItemIndex property.
        var currentItemIndexBinding = new Binding("CurrentItemIndex")
        {
            Mode = BindingMode.TwoWay,
            Source = parameterVm,
            UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
        };
        valueGrid.SetBinding(DataGrid.SelectedIndexProperty, currentItemIndexBinding);
    }

    private void OnValueTextBoxInitialized(object sender, EventArgs e)
    {
        TextBox textBox = (TextBox)sender;
        DataGridRow parentRow = VisualTreeHelpers.FindAncestor<DataGridRow>(textBox);
        if (!(parentRow.Item is ParameterViewModel parameterVm))
            return;

        var valueBinding = new Binding("Value")
        {
            Mode = BindingMode.TwoWay,
            Source = parameterVm,
            ValidatesOnDataErrors = true
        };
        textBox.SetBinding(TextBox.TextProperty, valueBinding);
    }
}

此代码位于App.xaml.cs文件中,并使用VisualTreeHelpers类,您可以在this post on Rachel Lim's blog中找到该类。

如您所见,OnArrayDataGridInitialized事件处理程序找到父DataGrid控件,然后检索其DataContext并将其转换为ParameterViewModel。由此,它创建所需的绑定以将视图连接到对象。

PrameterViewModel类中,我必须添加一个名为ObservableCollection<object>的{​​{1}}属性,如果该属性是一个数组,它将保存各个数组元素。我还添加了一个名为Items的{​​{1}}属性和一个名为object的{​​{1}}属性。这些在上面的XAML和CurrentItem事件处理程序中被引用。

int属性的设置器现在看起来像这样:

CurrentItemIndex

在构造函数中,初始化Initialized集合后,我订阅了它的Value事件;这是我处理这些事件的方式:

    public object Value
    {
        get => _value;
        set
        {
            if (_value == value)
                return;

            if (_value is INotifyPropertyChanged npc)
                npc.PropertyChanged -= OnItemPropertyChanged;

            _value = value;
            RaisePropertyChanged();

            npc = _value as INotifyPropertyChanged;
            if (npc != null)
                npc.PropertyChanged += OnItemPropertyChanged;

            IsDirty = true;
            RaisePropertyChanged(nameof(IsValid));
        }
    }

    private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        RaisePropertyChanged(nameof(Value));
    }

最后,我必须对Items类进行一些更改才能使所有内容正确显示和更新。这是CollectionChanged构造函数:

    private void OnItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.OldItems != null)
        {
            foreach (object item in e.OldItems)
            {
                if (item is INotifyPropertyChanged npc)
                    npc.PropertyChanged -= OnItemPropertyChanged;
            }
        }

        if (e.NewItems != null)
        {
            foreach (object item in e.NewItems)
            {
                if (item is INotifyPropertyChanged npc)
                    npc.PropertyChanged += OnItemPropertyChanged;
            }
        }

这是构造函数调用的ParameterViewModel方法:

ParameterViewModel

就是这样。在XAML中无法完成全部操作,因为 public ParameterViewModel( string objectName, string method, string name, Type type, object value, bool isNameReadonly = false) : this(findContext) { if (string.IsNullOrEmpty(objectName)) throw new ArgumentNullException(nameof(objectName)); if (string.IsNullOrEmpty(method)) throw new ArgumentNullException(nameof(method)); _name = name ?? throw new ArgumentException(nameof(name)); _type = type ?? throw new ArgumentNullException(nameof(type)); _elementType = _type.GetElementType() ?? _type; _value = value; if (_value != null && _type.IsArray && ((Array)_value).Length > 0) LoadItems(_value, _elementType); RaisePropertyChanged(nameof(IsValid)); } 控件不像普通控件那样参与可视树结构。程序运行时,您可以使用Snoop正常导航到所有内容,但是绑定不能正常继承。这可以正常工作,并且使用用于自定义类型的自定义模板,用户可以轻松阅读和浏览所有内容。