Sinatr
Sinatr

Reputation: 21969

How can I modify xaml resource at run-time?

I have many very similar resources in xaml (varying by a tiny bit: name of property in bingings, static text in header, etc.) which are quite big and complex:

<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>

And my aim is to have just this:

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

where resource become kind of template. But then I need to somehow define a parameter to alter each such resource (e.g. to modify property name in the binding) before using it.


My current idea is to manipulate resources as text:

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

But defining xaml strings in code-behind doesn't sounds good, they should be a part of xaml, where they are used.

So I had this brilliant idea:

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

But unfortunately XamlWriter is very limited, it didn't worked, the restored this way xaml is totally unusable.

Then I had a thought to define resource as string:

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

But multiline string and special character in xaml making this approach looking very ugly. Don't try it at home.


Ideally I want to define resources as before (to have intellisence and stuff) and just want to modify them at run-time somehow, therefore my question.

The localized case of the problem (it's quite the same) is to have parameter in DataTemplate. I was asking question about dynamic columns earlier, that's why I have so many similar resources defined currently and trying to find a solution again.


I forgot to add a concrete example of resource as well as some form of 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>

some columns are added

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

where Columns collection defines which columns can be shown, which are hidden, their width, etc.

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

To have 100 columns I have to define 100 "columnX" resources, but they are very similar. My challenge is to define just one and then somehow alter dynamic parts (in this case to change "Header1" to "Header2" and "Value1" to "Value2").

Upvotes: 2

Views: 409

Answers (2)

grek40
grek40

Reputation: 13438

More focusing on your GridView example instead of the question title.

You could create a UserControl or Custom Control in order to define the appearence of a cell content.

Within the custom control, you can define your whole shared styling and define dependency properties for things that should be different per cell.

As an example, here is a custom control MyCellContent that allows to bind a Text property or to bind a MyTextPropertyName property which will automatically create a binding on the Text property, redirecting to whatever MyTextPropertyName specifies:

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));
}

And in 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>

Now, this is something to build upon.

For example, you could create a CellTemplateSelector that creates a cell template, where the Text property binding is determined by another property value of the data item:

// 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
        };
    }
}

Another different way to use the MyCellContent control would be a customized MyGridviewColumn, that does basically the same as the template selector above, but instead of a data driven property selection, it allows to specify a binding to be used on the Text property:

/// <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
        };
    }
}

Usage example with some test data:

<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>

The result:

A gridview where the first column displays the values "Value 1" and "Value 4", because it selects the value from "Property1" in the first row and from "Property2" in the second row. So the displayed data is driven by two data dimensions: the specified property name and the target property value.

The second column displays the values "Value 3" and "Value 4", because it utilizes the specified binding expression "{Binding Property2}". So the displayed data is driven by the specified binding expression, which could refer to a data property or anything else that's legally binding within a data grid cell.

Upvotes: 1

Sinatr
Sinatr

Reputation: 21969

I have found a way to write xaml which:

  • has designer support
  • has intellisense support;
  • can be modified at run-time.

For this xaml (resources) needs to be put into separate ResourceDictionary which Build property set to Embedded Resource

The content will looks like this:

<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>

And the code to load and modify

// 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);

I personally don't use designer at all (only to quickly navigating) and write all xaml with hands. Having no designer support is not a problem for me.

Intellisense support and seeing mistakes at compile time are very handy. Disregards of build action the xaml will be fully validated, which is a good thing (compared to saving xaml in string in code behind or in a text-file).

Separating resources from window/user control are sometimes problematic, e.g. if there are bindings with ElementName or references to other resources (which are not moved to resource dictionary), etc. I have currently issue with BindingProxy, therefore this solution is not a final one.

Upvotes: 1

Related Questions