【问题标题】:How can I modify xaml resource at run-time?如何在运行时修改 xaml 资源?
【发布时间】:2020-12-04 02:42:13
【问题描述】:

我在 xaml 中有许多非常相似的资源(略有不同:bingings 中的属性名称、标题中的静态文本等),它们非常庞大且复杂:

<Window.Resource>
    <A x:Key="a1"> ... </A>
    <A x:Key="a2"> ... </A>
    ...
    <B x:Key="b1"> ... />
    <B x:Key="b2"> ... />
    ...
    <C x:Key="c1"> ... />
    ...
</Window.Resource>

而我的目标就是拥有这个:

    <A x:Key="a" ... />
    <B x:Key="b" ... />
    <C x:Key="c" ... />
    ...

资源成为一种模板。但是我需要在使用它之前以某种方式定义一个 参数 来更改每个此类资源(例如修改绑定中的属性名称)。


我目前的想法是将资源作为文本来操作:

var xaml = @"... Text = ""{Binding %PROPERTY%}"" ...";
xaml = xaml.Replace("%PROPERTY%", realPropertyName);
view.Content = XamlReader.Parse(xaml)

但是在代码隐藏中定义 xaml 字符串听起来不太好,它们应该是 xaml 的一部分,在那里使用它们。

所以我有了这个绝妙的想法:

// get some resource and restore xaml string for it, yay!
var xaml = XamlWriter.Save(FindResource("some resource"));

但不幸的是XamlWriter 非常有限,它没有工作,恢复这种方式 xaml 完全无法使用。

然后我想到了将资源定义为字符串:

<clr:String x:Key="a">...</clr:String>

但是 multiline 字符串和 xaml 中的特殊字符使这种方法看起来很丑陋。不要在家里尝试。


理想情况下,我想像以前一样定义资源(拥有智能和东西),只是想在运行时以某种方式修改它们,因此我的问题。

问题的本地化案例(完全一样)是在DataTemplate 中有参数。我之前问过关于dynamic columns 的问题,这就是为什么我目前定义了这么多类似的资源并试图再次找到解决方案。


我忘了添加资源的具体示例以及某种形式的 MCVE:

<Window.Resources>
    <GridViewColumn x:Key="column1">
        <GridViewColumn.Header>
            <DataTemplate>
                <TextBlock Text="Header1" />
            </DataTemplate>
        </GridViewColumn.Header>
        <GridViewColumn.CellTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Value1}" />
            </DataTemplate>
        </GridViewColumn.CellTemplate>
    </GridViewColumn>
    ...
    ... more similar columns
    ...
</Window.Resources>
<ListView x:Name="listView" ItemsSource="{Binding Items}">
    <ListView.View>
        <GridView />
    </ListView.View>
</ListView>

添加了一些列

var view = (GridView)listView.View;
foreach(var column in Columns.Where(o => o.Show))
    view.Columns.Add((GridViewColumn)FindResouce(column.Key));

Columns 集合定义了哪些列可以显示,哪些是隐藏的,它们的宽度等。

public class Column
{
    public string Key { get; set; } // e.g. "column1"
    public bool Show { get; set; }
    ...
}

要拥有 100 列,我必须定义 100 个"columnX" 资源,但它们非常相似。我的挑战是只定义一个,然后以某种方式更改动态部分(在这种情况下,将"Header1" 更改为"Header2" 和"Value1" 更改为"Value2")。

【问题讨论】:

  • 这些资源会根据什么改变,即改变该名称的标准是什么?顺便说一句,在运行时更改名称听起来很可疑。我有一种感觉,这可能部分是设计问题,但现在说还为时过早。另一种方法是将这些导出到单独的字典中,并尝试根据...加载它们
  • @XAMlMAX,我已经用示例编辑了帖子。虽然我的两篇文章都是关于动态列的,但我可以将解决方案(如果存在)用于其他事情,以减少 xaml 的冗长。
  • 这听起来就像你想在 xaml 中拥有你的模型。
  • 我认为我没有完全理解您的意图,因为我没有看到问题所在。为什么不使用简单的DataTemplateSelector?你提前知道源数据模型的所有属性吗?

标签: c# wpf xaml resources datatemplate


【解决方案1】:

我找到了一种编写 xaml 的方法:

  • 有设计师支持
  • 具有智能感知支持;
  • 可以在运行时修改。

为此,xaml(资源)需要放入单独的ResourceDictionary,其中Build 属性设置为Embedded Resource

内容将如下所示:

<ResourceDictionary ...>
    <GridViewColumn x:Key="test" Header="%HEADER%"> <!-- placeholder for header -->
        <GridViewColumn.CellTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding %CELL%}" /> <!-- placeholder for cell property name -->
            </DataTemplate>
        </GridViewColumn.CellTemplate>
    </GridViewColumn>
</ResourceDictionary>

以及要加载和修改的代码

// get resource stream
var element = XElement.Load(Assembly.GetExecutingAssembly().GetManifestResourceStream("..."));

// get xaml as text for a specified x:Key
var xaml = element.Descendants().First(o => o.Attributes().Any(attribute => attribute.Name.LocalName == "Key")).ToString();

// dynamic part
xaml = xaml.Replace("%HEADER%", "Some header");
xaml = xaml.Replace("%CELL%", "SomePropertyName");

// xaml to object
var context = new ParserContext();
context.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
var column = (GridViewColumn)XamlReader.Parse(xaml, context);

view.Columns.Add(column);

我个人根本不使用设计器(仅用于快速导航)并用手编写所有 xaml。没有设计师支持对我来说不是问题。

Intellisense 支持和在编译时查看错误非常方便。忽略构建操作,xaml 将被完全验证,这是一件好事(与将 xaml 保存在 string 中的代码或文本文件中相比)。

从窗口/用户控件中分离资源有时是有问题的,例如如果有与ElementName 的绑定或对其他资源的引用(未移动到资源字典)等。我目前有issue with BindingProxy,因此这个解决方案不是最终解决方案。

【讨论】:

    【解决方案2】:

    更多地关注您的 GridView 示例而不是问题标题。

    您可以创建用户控件或自定义控件来定义单元格内容的外观。

    在自定义控件中,您可以定义整个共享样式,并为每个单元格应该不同的事物定义依赖属性。

    例如,这是一个自定义控件MyCellContent,它允许绑定Text 属性或绑定MyTextPropertyName 属性,这将自动在Text 属性上创建绑定,重定向到@987654325 @指定:

    public class MyCellContent : Control
    {
        static MyCellContent()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(MyCellContent), new FrameworkPropertyMetadata(typeof(MyCellContent)));
        }
    
    
        // Specify a property name that should be used as binding path for Text
        public string MyTextPropertyName
        {
            get { return (string)GetValue(MyTextPropertyNameProperty); }
            set { SetValue(MyTextPropertyNameProperty, value); }
        }
    
        public static readonly DependencyProperty MyTextPropertyNameProperty =
            DependencyProperty.Register("MyTextPropertyName", typeof(string), typeof(MyCellContent), new PropertyMetadata(null, OnTextPropertyChanged));
    
    
        private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            BindingOperations.SetBinding(d, TextProperty, new Binding(e.NewValue as string));
        }
    
        // The text to be displayed
        public string Text
        {
            get { return (string)GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }
    
        public static readonly DependencyProperty TextProperty =
            DependencyProperty.Register("Text", typeof(string), typeof(MyCellContent), new PropertyMetadata(null));
    }
    

    在 Themes/Generic.xaml 中

    <Style TargetType="{x:Type local:MyCellContent}">
        <!-- Demonstrate the power of custom styling -->
        <Setter Property="Background" Value="Yellow"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:MyCellContent}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <TextBlock Text="{TemplateBinding Text}" />
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    

    现在,这是可以构建的。

    例如,您可以创建一个创建单元格模板的CellTemplateSelector,其中Text 属性绑定由数据项的另一个属性值确定:

    // Specialized template selector for MyGenericData items
    public class GridViewColumnCellTemplateSelector : DataTemplateSelector
    {
        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            var data = item as MyGenericData;
            return CreateCellTemplate(data.MyTargetPropertyName);
        }
    
        /// <summary>
        /// Create a template with specified binding path
        /// </summary>
        private DataTemplate CreateCellTemplate(string targetPropertyName)
        {
            FrameworkElementFactory myCellContentFactory = new FrameworkElementFactory(typeof(MyCellContent));
    
            myCellContentFactory.SetValue(MyCellContent.MyTextPropertyNameProperty, targetPropertyName);
    
            return new DataTemplate
            {
                VisualTree = myCellContentFactory
            };
        }
    }
    

    使用MyCellContent 控件的另一种不同方式是自定义MyGridviewColumn,它基本上与上面的模板选择器相同,但不是数据驱动的属性选择,它允许指定要在 Text 属性上使用的绑定:

    /// <summary>
    /// Pre-Templated version of the GridViewColumn
    /// </summary>
    public class MyGridviewColumn : GridViewColumn
    {
        private BindingBase _textBinding;
    
        public BindingBase TextBinding
        {
            get { return _textBinding; }
            set
            {
                if (_textBinding != value)
                {
                    _textBinding = value;
                    CellTemplate = CreateCellTemplate(value);
                }
            }
        }
    
        /// <summary>
        /// Create a template with specified binding
        /// </summary>
        private DataTemplate CreateCellTemplate(BindingBase contentBinding)
        {
            FrameworkElementFactory myCellContentFactory = new FrameworkElementFactory(typeof(MyCellContent));
    
            myCellContentFactory.SetBinding(MyCellContent.TextProperty, contentBinding);
    
            return new DataTemplate
            {
                VisualTree = myCellContentFactory
            };
        }
    }
    

    带有一些测试数据的使用示例:

    <Window.Resources>
        <x:Array x:Key="testItems" Type="{x:Type local:MyGenericData}">
            <local:MyGenericData Property1="Value 1" Property2="Value 3" MyTargetPropertyName="Property1"/>
            <local:MyGenericData Property1="Value 2" Property2="Value 4" MyTargetPropertyName="Property2"/>
        </x:Array>
    
        <local:GridViewColumnCellTemplateSelector x:Key="cellTemplateSelector"/>
    </Window.Resources>
    

    ...

    <ListView ItemsSource="{Binding Source={StaticResource testItems}}">
        <ListView.View>
            <GridView>
                <GridViewColumn CellTemplateSelector="{StaticResource cellTemplateSelector}" Header="ABC" Width="100" />
                <local:MyGridviewColumn TextBinding="{Binding Property2}" Header="DEF" Width="100" />
            </GridView>
        </ListView.View>
    </ListView>
    

    结果:

    第一列显示值“Value 1”和“Value 4”的网格视图,因为它从第一行的“Property1”和第二行的“Property2”中选择值。所以显示的数据是由两个数据维度驱动的:指定属性名和目标属性值。

    第二列显示值“Value 3”和“Value 4”,因为它使用了指定的绑定表达式“{Binding Property2}”。因此,显示的数据由指定的绑定表达式驱动,该表达式可以引用数据属性或在数据网格单元格中合法绑定的任何其他内容。

    【讨论】:

    • 答案的核心是在代码后面创建模板(尽管它已被贬低)并使用动态绑定(稍后将提供绑定源或属性名称)。这是一个有趣的方法,谢谢。我会记住这一点,如果将 xaml 作为文本编辑失败,我会重新考虑。
    • 我想核心是创建一个单独的可重用控件,其中包含所有的花里胡哨,因此模板本身变得尽可能小和干净。然后使用任何方法来动态创建模板(FrameworkElementFactory 可能不是最好的主意,这只是一个主意)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多