首页 文章

有没有办法在XAML中为用户控件(复合控件)中的控件设置DataTemplate

提问于
浏览
1

我创建了一个包含工具栏和Datagrid的复合用户控件,并将它们公开为公共属性 . 有没有办法在工具栏中添加新按钮并在XAML中为Datagrid设置DataTemplate,而不是在代码隐藏文件中实现它们,如果我在另一个窗口或用户控件中使用此用户控件?

我找到了类似的链接here,但不知道该怎么做 . 请帮忙 .

这是Xaml:

<UserControl x:Class="CRUDDataGrid1"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         mc:Ignorable="d" >
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <ToolBarTray Grid.Row="0" >
        <ToolBar x:Name="tb">
            <Button x:Name="Add" Content="Add">                    
            </Button>
        </ToolBar>
    </ToolBarTray>
    <DataGrid Grid.Row="1" x:Name="dg">

    </DataGrid>
</Grid>
</UserControl>

这是代码隐藏:

public partial class CRUDDataGrid1 : UserControl
{
    public ToolBar ToolBar { get; set; }
    public DataGrid DataGrid { get; set; }
    public ObservableCollection<DataGridColumn> Columns { get; private set; } //edited

    public CRUDDataGrid1()
    {
        InitializeComponent();
        ToolBar = tb;
        DataGrid = dg;
        Columns = dg.Columns;  //edited
    }
}

我想在另一个用户控件中使用此用户控件,如下所示:

<UserControl x:Class="UserControl1" ...>
<Grid>
    <local:CRUDDataGrid1>
        <local:CRUDDataGrid1.ToolBar>
            <Button x:Name="Delete" Content="Delete">
            </Button>
        </local:CRUDDataGrid1.ToolBar>
        <local:CRUDDataGrid1.DataGrid ItemsSource="{Binding Customers}">
            <local:CRUDDataGrid1.Columns>
                <DataGridTextColumn Header="First Name" Binding="{Binding XPath=@FirstName}" />
                <DataGridTextColumn Header="Last Name" Binding="{Binding XPath=@LastName}" />
            <local:CRUDDataGrid1.Columns>
        </local:CRUDDataGrid1.DataGrid>
    </local:CRUDDataGrid1>
</Grid>
</UserControl>

1 回答

  • 2

    1 Foreword

    拥有一个拥有ToolBar的子控件并希望该子控件的父控件将工具栏项添加到子控件所拥有的ToolBar中,这是一个糟糕设计的标志 . 对您而言,主要和最重要的建议是重新考虑您的软件设计,以避免这种共享/拆分初始化 .

    在几乎任何情况下,您都希望最顶层控件拥有的工具栏(如主窗口)或文档窗口(如果您的应用程序具有MDI或浮动窗口) . 工具栏项目将从该窗口内的相应控件中收集;例如,复制/粘贴/等 . 来自文档编辑器控件的操作,用于从其他位置创建或加载新文档的操作等 .

    旁注:通常,这样的设计是因为新手WPF程序员想要以老式的方式使用Click事件处理程序来实现按钮操作 . 这样的Click事件处理程序创建代码依赖项,只要它们只包含在一个(自定义)控件中,一切都很好 . 但是,只要这不再可行(例如,当一个动作应该显示为工具栏按钮或者应该通过菜单触发相同的动作时),尝试坚持使用Click事件处理程序将导致复杂的代码,即使对于简单用户界面可能会导致严重的头痛......

    WPF中避免那些讨厌的Click事件处理程序的机制是命令,或者更具体地说是RoutedCommands . 公平地说,必须注意RoutedCommands有自己的share of challenges . 然而,许多优秀的人写了很多关于使用WPF的RoutedCommands以及如何扩展其功能的有趣和重要的事情,所以我可以给出的唯一合理的建议是如果你想要/需要了解更多,可以使用Google的强大功能 .


    2 Answering the question, but not solving the underlying design issue

    要创建一个工具栏,其中包含在不同位置定义的工具栏项集合,而不希望在同一个ToolBarTray中使用多个工具栏带,则需要在某些时候将工具栏项集合合并到单个列表中 . 这可以在代码隐藏中以某种方式完成,也可以在自定义IMultiValueConverter的帮助下在XAML中完成 .

    自定义IMultiValueConverter - 让我们称之为 MergeCollectionsConverter - 将对任何数据类型都是不可知的 . 它只需要一些IEnumerables并将所有元素添加到结果列表中 . 它甚至接受不是IEnumerable的对象,这些对象本身将被添加到结果列表中 .

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using System.Windows.Data;
    using System.Windows.Documents;
    
    namespace MyStuff
    {
        public class MergeCollectionsConverter : IMultiValueConverter
        {
            public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
            {
                if (values == null) return null;
    
                List<object> combinedList = new List<object>();
                foreach (object o in values)
                {
                    if (o is IEnumerable)
                        combinedList.AddRange( ((IEnumerable) o).Cast<object>() );
                    else
                        combinedList.Add(o);
                }
                return combinedList;
            }
    
            public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
            {
                throw new NotImplementedException();
            }
        }
    }
    

    我还假设CRUDDataGrid1中的ToolBar应该来自两个工具栏项集合 . 具有默认工具栏项的第一个集合在CRUDDataGrid1中定义 . 第二个集合应允许其他控件在默认项后添加其他工具栏项;因此,该集合必须可公开访问 .

    根据问题中的示例代码,您的CRUDDataGrid1类可能如下所示(仅考虑工具栏,它不代表完整的类):

    CRUDDataGrid1.cs:

    public partial class CRUDDataGrid1 : UserControl, INotifyPropertyChanged
    {
        public ObservableCollection<object> AdditionalToolbarItems { get { return _additionalToolbarItems; } }
        private readonly ObservableCollection<object> _additionalToolbarItems = new ObservableCollection<object>();
    
        public event PropertyChangedEventHandler PropertyChanged;
    
        public UserControl1()
        {
            InitializeComponent();
    
            _additionalToolbarItems.CollectionChanged +=
                (sender, eventArgs) =>
                {
                    PropertyChangedEventHandler handler = PropertyChanged;
                    if (handler != null)
                        handler(this, new PropertyChangedEventArgs("AdditionalToolbarItems"));
                };
    
            ...other constructor code...
        }
    }
    

    CRUDDataGrid1.xaml:

    <DockPanel>
        <ToolBarTray DockPanel.Dock="Top">
            <ToolBar>
                <ToolBar.Resources>
                    <DataTemplate DataType="{x:Type My:UseCommand}">
                        <Button
                            Style="{StaticResource {x:Static ToolBar.ButtonStyleKey}}"
                            Command="{Binding Command}"
                            CommandTarget="{Binding Target}"
                            CommandParameter="{Binding Parameter}"
                            Content="{Binding Command.Text}"
                        />
                    </DataTemplate>
    
                    <My:MergeCollectionsConverter x:Key="convToolbarItems" />
    
                    <x:Array x:Key="defaultToolbarItems" Type="{x:Type sys:Object}">
                        <My:UseCommand Command="ApplicationCommands.New" />
                        <My:UseCommand Command="ApplicationCommands.Cut" />
                        <My:UseCommand Command="ApplicationCommands.Paste" />
                    </x:Array>
                </ToolBar.Resources>
    
                <ToolBar.ItemsSource>
                    <MultiBinding Converter="{StaticResource convToolbarItems}">
                        <Binding Source="{StaticResource defaultToolbarItems}" />
                        <Binding Path="AdditionalToolbarItems" ElementName="crudDataGrid1" />
                    </MultiBinding>
                </ToolBar.ItemsSource>
    
                </ToolBar>
        </ToolBarTray>
        <DataGrid x:Name="dg" />
    </DockPanel>
    

    第一个集合是ToolBar资源目录中的'static resource',由资源键“ defaultToolbarItems ”标识 . 第二个是CRUDDataGrid1的属性 AdditionalToolbarItems 提供的集合 . 使用上述转换器的<MultiBinding>,合并列表绑定到ToolBar的ItemsSource .

    查看AdditionalToolbarItems属性的C#源代码,您将注意到INotifyPropertyChanged实现和CollectionChanged事件的处理程序 . 这是为什么?请记住,AdditionalToolbarItems是只读属性 . 在完全构造CRUDDataGrid1控件时,已设置数据绑定,并且多绑定已处理AdditionalToolbarItems . 它永远不会再次处理,因为属性本身永远不会改变它的值(它将始终引用相同的ObservableCollection) . 为了使<MultiBinding>在AdditionalToolbarItems集合的内容发生更改时重新评估绑定属性,代码需要监听CollectionChanged事件并在AdditionalToolbarItems的内容发生更改时触发PropertyChanged事件,这反过来将导致< MultiBinding>重新评估绑定属性 .

    您还将注意 My:UseCommand 元素的使用,而不是使用<Button> . 好吧,你可以使用<Button>,它也可以 . 直到您的应用程序想要一次使用多个工具栏共享相同的默认按钮 - 在这种情况下您遇到问题:按钮是一个控件,因此具有 one 父UI元素 . 您无法在多个工具栏中共享按钮控件,因为控件只能由 one 父UI元素作为子项拥有 . 因此,使用RoutedCommands代替按钮控件(如果您在下面的第3部分中阅读'real'解决方案,另一个同样重要的原因将变得明显) . 但是,从技术上讲,没有什么可以阻止你声明<Button>元素 - 你甚至可以将<My:UseCommand>和<Button>(和其他元素混合,只要这些元素可以在工具栏中呈现) .

    UseCommand 是一个非常小而简单的类,它允许您告诉使用哪个命令(如果需要,还可以使用可选的CommandTarget和CommandParameter):

    namespace MyStuff
    {
        public class UseCommand
        {
            public System.Windows.Input.ICommand Command { get; set; }
            public System.Windows.IInputElement Target { get; set; }
            public object Parameter { get; set; }
        }
    }
    

    ToolBar需要一个DataTemplate来正确显示存储在UseCommand中的命令及其参数 . 您可以在上面的XAML代码中将此DataTemplate视为ToolBar资源字典的一部分 .

    有了这些东西,在 UserControl1 中使用CRUDDataGrid1并添加其他工具栏项可能如下所示:

    <UserControl x:Class="UserControl1" ...>
        <Grid>
            <local:CRUDDataGrid1>
                <local:CRUDDataGrid1.AdditionalToolbarItems x:Name="cdg">
                    <My:UseCommand Command="ApplicationCommands.Close" CommandTarget="{Binding ElementName=cdg}" />
                    <My:UseCommand Command="ApplicationCommands.New" CommandTarget="{Binding ElementName=cdg}" />
                </local:CRUDDataGrid1.AdditionalToolbarItems>
                ...
            </local:CRUDDataGrid1.DataGrid>
        </Grid>
    </UserControl>
    

    对于我的示例代码,我使用了System.Windows.Input.ApplicationCommands提供的命令 . 你可以自己滚动你自己的命令(我们将在下面看到) . 还要注意CommandTarget属性的演示用法 . 是否需要使用此属性需要了解RoutedCommands的工作方式,并且主要取决于UI的可视/逻辑树中哪个元素为该特定命令 Build 处理程序的位置 .


    3 Using RoutedCommands to solve the design issue and the question

    阅读第2节后,您应该已经认识到RoutedCommands将帮助您将用户可调用操作的提供与实际UI表示中的任何组件分开,并且这可以帮助您避免关于有些复杂的组合的shenanigans来自不同来源的ToolBar . 因为,CRUDDataGrid1实际上需要为您的GUI提供的是工具栏(或菜单或任何其他命令调用者)的命令 .

    从我的源代码中可以看出,CRUDDataGrid1负责执行“ Add ”操作,而UserControl1负责“ Delete ”操作 . 这两个操作都应出现在同一工具栏中 .

    让我们看看CRUDDataGrid1的“添加”操作 . 首先,要通过RoutedCommand使此操作可以调用,显然需要提供适当的RoutedCommand对象 . 您可以选择.NET提供的RoutedCommands之一(在ApplicationCommandsComponentCommandNavigationCommand中声明) . 但是,这并不总是一个好主意 . 诸如ApplicationCommands.Copy之类的常用命令几乎可以由支持剪贴板操作的任何控件执行,并且知道哪个实际控件将处理这样的命令的调用需要知道如何通过可视树路由RoutedCommands以及logcial focus如何影响这个命令 . 路由 . 因此,有时将您自己的RoutedCommand定义为公共静态属性更容易 - 我们将在此处执行“添加”操作:

    public partial class CRUDDataGrid1 : UserControl
    {
        public static readonly RoutedCommand AddCommand = new RoutedCommand("CRUDDataGridCommand.Add", typeof(CRUDDataGrid1));
    
        public UserControl1()
        {
            InitializeComponent();
    
            CommandBindings.Add(
                new CommandBinding(
                    AddCommand,
                    OnExecutedAddCommand,
                    CanExecuteAddCommand
                )
            );
    
            ...other constructor code...
        }
    
        private void CanExecuteAddCommand(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = ...here your code that decides whether the "Add" command can execute
                           (and thus whether any button which uses this command will be enabled/disabled)
        }
    
        private void CanExecuteAddCommand(object sender, ExecutedRoutedEventArgs e)
        {
            ...execute the "Add" action here...
        }
    }
    

    请注意构造函数中的命令绑定以及处理该命令的相应方法 . 只是为了避免混淆:作为CommandTarget的对象不需要实现命令绑定 . CommandTarget仅指定路由开始的可视/逻辑树中的对象 .

    虽然我没有在这里显示,但UserControl中关于 DeleteCommand 的实现遵循相同的模式 .

    public partial class UserControl1 : UserControl
    {
        public static readonly RoutedCommand DeleteCommand = new RoutedCommand("UserControl1Command.Delete", typeof(UserControl1));
    
        ...same implementation approach as demonstrated for CRUDDataGrid1.AddCommand...
    }
    

    现在,可以在UserControl1.xaml中完全创建ToolBar,而无需担心命令所代表的各个操作是如何执行的 . 注意,自从使用<Button>就可以了工具栏完全在UserControl1中创建,没有任何一个按钮可能与另一个控件"shared" . 另外,请注意缺少像UseCommand和MergeCollectionsConverter这样的辅助类,这些是我的答案第2部分中有些复杂的场景所必需的 .

    <UserControl x:Class="UserControl1" ...>
        <DockPanel>
            <ToolBarTray DockPanel.Dock="Top">
                <ToolBar>
                    <Button Content="Add" Command="{x:Static local:CRUDDataGrid1.AddCommand}" CommandTarget="{Binding ElementName=cdg}" />
                    <Button Content="Delete" Command="{x:Static local:UserControl1.DeleteCommand}" />
                </ToolBar>
            </ToolBarTray>
            <local:CRUDDataGrid1 x:Name="cdg" ItemsSource="{Binding Customers}">
                <local:CRUDDataGrid1.Columns>
                    <DataGridTextColumn Header="First Name" Binding="{Binding XPath=@FirstName}" />
                    <DataGridTextColumn Header="Last Name" Binding="{Binding XPath=@LastName}" />
                </local:CRUDDataGrid1.Columns>
            </local:CRUDDataGrid1>
        </DockPanel>
    </UserControl>
    

    CRUDataGrid1 应该直接从DataGrid类型继承(不是UserControl),根据需要实现扩展的CRUD功能 .

    通过让CRUDataGrid1仅为任何所需的用户操作提供RoutedCommands,您和团队中的任何其他人都可以自由决定在GUI中使用RoutedCommands的位置 - 在工具栏,菜单或其他方面 . 您可以使用相同的命令使用多个按钮 - 没有问题 . RoutedCommands背后的基础结构还将根据绑定到命令的CanExecute方法的结果自动启用/禁用此类按钮 .

    在这里给出的例子中,我确实让CRUDataGrid1和UserControl1提供了RoutedCommands . 但是如果你有许多命令和更复杂的软件,那么就没有什么可以反对定义这些命令的中心位置(类似于Microsoft对.NET框架提供的RoutedCommands所做的那样) .

相关问题