7

I'm trying to load a Grid with UIElements and/or FrameworkElements that have Style and DataTriggers.

The following is a simple native XAML which works as expected; toggling the check box through the 3 states give three different styles to the Rectangle (see image at bottom).

<Grid
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:XamlVsLoad"
    mc:Ignorable="d"
    Height="450" Width="800">
    <CheckBox Name="MyCheck" IsChecked="True" Content="Checkbox" IsThreeState="True" Margin="10,10,0,0" Width="200"/>
    <Rectangle Height="100" Width="100" StrokeThickness="5" Margin="10,50,100,100">
        <Rectangle.Style>
            <Style TargetType="Rectangle">
                <Setter Property="Fill" Value="White"/>
                <Setter Property="Stroke" Value="Black"/>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding ElementName=MyCheck, Path=IsChecked}" Value="True" >
                        <Setter Property="Fill" Value="Orange"/>
                        <Setter Property="Stroke" Value="Blue"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding ElementName=MyCheck, Path=IsChecked}" Value="False" >
                        <Setter Property="Fill" Value="Blue"/>
                        <Setter Property="Stroke" Value="Orange"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </Rectangle.Style>
    </Rectangle>
</Grid>

It also works when I save the same text to a file and load it dynamically into Window's Viewboxs with XAML like this:

<Grid.ColumnDefinitions>
    <ColumnDefinition Width="100"/>
    <ColumnDefinition Width="100"/>
    <ColumnDefinition Width="100"/>
    <ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<DockPanel Grid.Column="0">
    <DockPanel>
        <Label Content="Native" DockPanel.Dock="Top" HorizontalAlignment="Center"  Margin="10,10,10,10" />
        <Viewbox Name="NativeViewbox" Height="auto" Width="200" DockPanel.Dock="Top"/>
           <!-- Native Grid Omitted for brevity 3 uniquely named checkboxes -->
        </Viewbox>
</DockPanel>
<DockPanel Grid.Column="1">
    <DockPanel>
        <Label Content="Dynamic" DockPanel.Dock="Top"  HorizontalAlignment="Center"  Margin="10,10,10,10" />
        <Viewbox Name="DynamicViewbox" Height="auto" Width="200" DockPanel.Dock="Top"/>
    </DockPanel>
</DockPanel>
<DockPanel Grid.Column="2">
    <DockPanel>
        <Label Content="Clone" DockPanel.Dock="Top" HorizontalAlignment="Center"  Margin="10,10,10,10" />
        <Viewbox Name="CloneViewbox" Height="auto" Width="200" DockPanel.Dock="Top" />
    </DockPanel>
</DockPanel>
<DockPanel Grid.Column="3">
    <DockPanel>
        <Label Content="Saved" DockPanel.Dock="Top" HorizontalAlignment="Center"  Margin="10,10,10,10" />
        <Viewbox Name="SavedViewbox" Height="auto" Width="200" DockPanel.Dock="Top"/>
    </DockPanel>
</DockPanel>

However, if I try to copy/clone/deepcopy (I've tried several different methods) the FrameworkElements/UIElements from one Grid/container to a new one, the style no longer works. Here are the various ways I'm currently loading and cloning them:

public partial class ReadCopy : Window {
    public ReadCopy() {
        InitializeComponent();
        // test the dynamic load
        Grid NativeXaml = ReadGrid("C:\\XamlFiles\\LoadXaml.xaml");
        DynamicViewbox.Child = NativeXaml; // honors style

        // test the Clone load
        Grid CloneXaml = new Grid();
        foreach (FrameworkElement fe in NativeXaml.Children) CloneXaml.Children.Add(Clone(fe));
        CloneViewbox.Child = CloneXaml;    // doesn't honor style

        // test the save Clone and then load
        StringBuilder outstr = new StringBuilder();
        XmlWriterSettings settings = new XmlWriterSettings { Indent = true, OmitXmlDeclaration = true, IndentChars = "  ", NewLineChars = "\r\n", NewLineHandling = NewLineHandling.Replace };
        XamlDesignerSerializationManager dsm = new XamlDesignerSerializationManager(XmlWriter.Create(outstr, settings)) { XamlWriterMode = XamlWriterMode.Expression };
        XamlWriter.Save(CloneXaml, dsm);
        File.WriteAllText("C:\\XamlFiles\\SavedXaml.xaml", outstr.ToString());

        Grid SavedXaml = ReadGrid("C:\\XamlFiles\\SavedXaml.xaml");
        SavedViewbox.Child = SavedXaml;    // honors style and triggers again...
    }

    public Grid ReadGrid(string fn) {
        FileStream fs = new FileStream(fn, FileMode.Open);
        return XamlReader.Load(fs) as Grid;
    }

    public FrameworkElement Clone(FrameworkElement it) {
        FrameworkElement clone;
        using (var stream = new MemoryStream())  {
            XamlWriter.Save(it, stream);
            stream.Seek(0, SeekOrigin.Begin);
            clone = (FrameworkElement)XamlReader.Load(stream);
        }
        clone.Style = it.Style; // setting it or not has no effect
        return clone;
    }
}

The output of the beginning simple example grid's cloned XAML via XamlWriter.Save:

<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <CheckBox IsChecked="True" IsThreeState="True" Style="{x:Null}" Name="MyCheck" Width="200" Margin="10,10,0,0">Checkbox</CheckBox>
  <Rectangle StrokeThickness="5" Width="100" Height="100" Margin="10,50,100,100">
    <Rectangle.Style>
      <Style TargetType="Rectangle">
        <Style.Triggers>
          <DataTrigger Binding="{Binding Path=IsChecked, ElementName=MyCheck}" Value="True">
            <Setter Property="Shape.Fill">
              <Setter.Value>
                <SolidColorBrush>#FFFFA500</SolidColorBrush>
              </Setter.Value>
            </Setter>
            <Setter Property="Shape.Stroke">
              <Setter.Value>
                <SolidColorBrush>#FF0000FF</SolidColorBrush>
              </Setter.Value>
            </Setter>
          </DataTrigger>
          <DataTrigger Binding="{Binding Path=IsChecked, ElementName=MyCheck}" Value="False">
            <Setter Property="Shape.Fill">
              <Setter.Value>
                <SolidColorBrush>#FF0000FF</SolidColorBrush>
              </Setter.Value>
            </Setter>
            <Setter Property="Shape.Stroke">
              <Setter.Value>
                <SolidColorBrush>#FFFFA500</SolidColorBrush>
              </Setter.Value>
            </Setter>
          </DataTrigger>
        </Style.Triggers>
        <Style.Resources>
          <ResourceDictionary />
        </Style.Resources>
        <Setter Property="Shape.Fill">
          <Setter.Value>
            <SolidColorBrush>#FFFFFFFF</SolidColorBrush>
          </Setter.Value>
        </Setter>
        <Setter Property="Shape.Stroke">
          <Setter.Value>
            <SolidColorBrush>#FF000000</SolidColorBrush>
          </Setter.Value>
        </Setter>
      </Style>
    </Rectangle.Style>
  </Rectangle>
</Grid>

While I can see it reformatted the Style, I don't understand why it no longer works when I set the Viewbox.Child with the clone. What's even more confusing is that the loading the saved file of the clone then works. Here's what all four (Native, Dynamic, Cloned, Saved Clone Reloaded) look like:

4 Examples

Can anyone explain how to properly preserve the style through a copy/clone?

codebender
  • 469
  • 1
  • 6
  • 23
  • 2
    Hint: have a look at the output of the XamlWriter.Save (write it to a temp file or something) – Dave M Jul 21 '19 at 03:08
  • I awarded the bounty, but don't really have an answer. If someone else has a more definitive answer, I'll attempt to award them another bounty. – codebender Aug 02 '19 at 12:19

1 Answers1

5

The Style on the Rectangle is triggered by the CheckBox. Because of this, Clone() will not work loading the child elements separately. To load them at the same time, clone their parent, the Grid itself. I reproduced your problem and this worked for me:

  Grid NativeXaml = ReadGrid();
  DynamicViewbox.Child = NativeXaml;

  Grid CloneXaml = (Grid)Clone(NativeXaml);
  CloneViewbox.Child = CloneXaml;
T K
  • 238
  • 1
  • 6
  • Interesting observation. However, I need to be able to clone/copy items from one container (grid, canvas...) to another. This answer doesn't explain why, once the items are individually copied, saved off, and reloaded, then work. – codebender Jul 26 '19 at 13:30
  • I have seen a lot of talk about how `XAMLReader.Load()` can sometimes break bindings, but no definitive documentation. If you are trying to build a reusable library where the two items are bound together like this, you may try combining them into a `UserControl` and clone the `UserControl` parent into your final container.. – T K Jul 26 '19 at 22:34
  • @codebender you are not really saving and reloading _individual_ items. If you see your code `XamlWriter.Save(CloneXaml, dsm);`, you are actually saving the whole `Grid` not individual items. – Suresh Aug 01 '19 at 18:39
  • @sthotakura each item is individually cloned onto that new Grid and then saved. Somehow the style is there, but not honored/recognized until fully reloaded. Placing every item that my have style attached to another element on my Grid/container into another container is not really an option. – codebender Aug 02 '19 at 12:17