Emitting the mc:Ignorable Instruction In Your WF4 XAML
Frequent forum guest Notre posed this question to the forums the other day noting that the XAML being produced from ActivityXamlServices.CreateBuilderWriter() was slightly different than the XAML being output from WorkflowDesigner.Save(). The reason for this stems from the fact that WorkflowDesigner leverages an additional internal type (which derives from XamlXmlWriter) in order to attach the mc:Ignorable attribute.
Why use mc:Ignorable?
From the source at MSDN:
The mc XML namespace prefix is the recommended prefix convention to use when mapping the XAML compatibility namespace https://schemas.openxmlformats.org/markup-compatibility/2006.
Elements or attributes where the prefix portion of the element name are identified as mc:Ignorable will not raise errors when processed by a XAML processor. If that attribute could not be resolved to an underlying type or programming construct, then that element is ignored. Note however that ignored elements might still generate additional parsing errors for additional element requirements that are side effects of that element not being processed. For instance, a particular element content model might require exactly one child element, but if the specified child element was in an mc:Ignorable prefix, and the specified child element could not be resolved to a type, then the XAML processor might raise an error.
Basically, this lets a XAML reader gracefully ignore any content marked from that namespace if it cannot be resolved. So, imagine a runtime scenario where we don’t want to load System.Activities.Presentation every time we read a WF XAML file that may contain viewstate. As a result, we use mc:Ignorable, which means the reader will skip that content when it does not have that assembly referenced at runtime.
This is what the output from the designer usually contains:
<Sequence
mc:Ignorable="sap"
mva:VisualBasic.Settings="Assembly references and imported namespaces serialized as XML namespaces"
xmlns="https://schemas.microsoft.com/netfx/2009/xaml/activities"
xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mva="clr-namespace:Microsoft.VisualBasic.Activities;assembly=System.Activities"
xmlns:sap="https://schemas.microsoft.com/netfx/2009/xaml/activities/presentation"
xmlns:scg="clr-namespace:System.Collections.Generic;assembly=mscorlib"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
<sap:WorkflowViewStateService.ViewState>
<scg:Dictionary x:TypeArguments="x:String, x:Object">
<x:Boolean x:Key="IsExpanded">True</x:Boolean>
</scg:Dictionary>
</sap:WorkflowViewStateService.ViewState>
<Persist sap:VirtualizedContainerService.HintSize="211,22" />
<Persist sap:VirtualizedContainerService.HintSize="211,22" />
<WriteLine sap:VirtualizedContainerService.HintSize="211,61" />
</Sequence>
mc:Ignorable will cause the ViewState and HintSize to be ignored.
Why do I have to worry about this?
If you use WorkflowDesigner.Save(), you don’t. If you want to be able to serialize the ActivityBuilder and have XAML which is what the designer produces, you need will need to add a XamlXmlWriter into the XamlWriter stack in order to get the right output. You may also worry about this if you are implementing your own storage and plan on writing some additional XAML readers or writers for additional extensibility and flexibility.
How Do I Get the Same Behavior?
The code below describes the same approach you would need to take to implement an XamlXmlWriter that does the same thing our internal type does. While I can’t copy and paste code, this does the same thing. We do two things:
- Override WriteNamespace() to collect all of the namespaces being emitted. We do this to specifically check for ones that we should ignore, and to also gather all of the prefixes to make sure we don’t have a collision
- Override WriteStartObject() to generate and write out the Ignorable attribute within the start (first) member for any namespaces we should ignore.
What namespaces do we ignore in the designer? Just one: https://schemas.microsoft.com/netfx/2009/xaml/activities/presentation
using System.Collections.Generic;
using System.IO;
using System.Xaml;
using System.Xml;
namespace IgnorableXamlWriter
{
class IgnorableXamlXmlWriter : XamlXmlWriter
{
HashSet<NamespaceDeclaration> ignorableNamespaces = new HashSet<NamespaceDeclaration>();
HashSet<NamespaceDeclaration> allNamespaces = new HashSet<NamespaceDeclaration>();
bool objectWritten;
bool hasDesignNamespace;
string designNamespacePrefix;
public IgnorableXamlXmlWriter(TextWriter tw, XamlSchemaContext context)
: base(XmlWriter.Create(tw,
new XmlWriterSettings { Indent = true, OmitXmlDeclaration = true }),
context,
new XamlXmlWriterSettings { AssumeValidInput = true })
{
}
public override void WriteNamespace(NamespaceDeclaration namespaceDeclaration)
{
if (!objectWritten)
{
allNamespaces.Add(namespaceDeclaration);
// if we find one, add that to ignorable namespaces
// the goal here is to collect all of them that might point to this
// if you had a broader set of things to ignore, you would collect
// those here.
if (namespaceDeclaration.Namespace == "https://schemas.microsoft.com/netfx/2009/xaml/activities/presentation")
{
hasDesignNamespace = true;
designNamespacePrefix = namespaceDeclaration.Prefix;
}
}
base.WriteNamespace(namespaceDeclaration);
}
public override void WriteStartObject(XamlType type)
{
if (!objectWritten)
{
// we should check if we should ignore
if (hasDesignNamespace)
{
// note this is not robust as mc could naturally occur
string mcAlias = "mc";
this.WriteNamespace(
new NamespaceDeclaration(
"https://schemas.openxmlformats.org/markup-compatibility/2006",
mcAlias)
);
}
}
base.WriteStartObject(type);
if (!objectWritten)
{
if (hasDesignNamespace)
{
XamlDirective ig = new XamlDirective(
"https://schemas.openxmlformats.org/markup-compatibility/2006",
"Ignorable");
WriteStartMember(ig);
WriteValue(designNamespacePrefix);
WriteEndMember();
objectWritten = true;
}
}
}
}
}
One note on the code above, it is noted that the generation of the namespace prefix “mc” is not robust. In the product code we will check to see if mc1, mc2, … are available up to mc1000. In that case we would then append a GUID for the ugliest XML namespace known to mankind. The chance of collision up to 1000 would be a highly extreme edge case.
How would I use this? The following code shows feeding this into a CreateBuilderWriter that is passed to XamlServices.Save()
StringBuilder sb = new StringBuilder();
XamlSchemaContext xsc = new XamlSchemaContext();
var bw = ActivityXamlServices.CreateBuilderWriter(
new IgnorableXamlXmlWriter(new StringWriter(sb), xsc));
XamlServices.Save(bw,
wd.Context.Services.GetService<ModelTreeManager>().Root.GetCurrentValue());
Comments
- Anonymous
December 10, 2009
Thanks again Matt! I didn't even ask for this blog post ;) . But I appreciate it!