📜 ⬆️ ⬇️

XamlWriter and Bindings

Goodnight Habra community.
I just received an invite to your company, and immediately decided to write something that might be useful for someone ... Do not judge strictly.

I am one of the developers of a single Open Source project, one of the main parts of which is a graphical editor, which must save vector graphics in XAML format as part of the WPF object model. During the development process, I ran into a problem. Bindings created from code (or from a loaded XAML file) are not saved back to XAML when attempting to serialize with a standard XamlWriter. As it turned out, this is the standard XamlWriter behavior described in MSDN. I tried to find a solution online, but found only one article on CodeProject . Unfortunately, as it turned out, this solution is not suitable for complex XAML documents for a number of reasons. I already started to consider writing my own serializer when I saw that the TemplateBinding extension is perfectly preserved by standard means, it gave me an idea that everything was not lost, and having armed with MS Reference Source Code and a debager, I began to study the problem. And that's what I got.


During the debugging process, I discovered that when the XamlWriter detects that DependencyProperty is connected to a Binding, it tries to find an ExpressionConverter registered for the specified type of binding, which makes this binding the type of MarkupExtension. If such a converter is found, then with its help, banding is brought to MarkupExtension, which is subsequently serialized.
')
Accordingly, the decision will be as follows. First, let's define the following class inherited from ExpressionConverter which will provide us with a conversion from Binding to MarkupExtension:
class BindingConvertor : ExpressionConverter
{
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType== typeof (MarkupExtension))
return true ;
else return false ;
}
public override object ConvertTo(ITypeDescriptorContext context,
System.Globalization.CultureInfo culture, object value , Type destinationType)
{
if (destinationType == typeof (MarkupExtension))
{
BindingExpression bindingExpression = value as BindingExpression;
if (bindingExpression == null )
throw new Exception();
return bindingExpression.ParentBinding;
}

return base .ConvertTo(context, culture, value , destinationType);
}
}


* This source code was highlighted with Source Code Highlighter .


After that, this converter must be registered. To do this, we define a static helper method and call it when the application is initialized. Like this:

static class EditorHelper
{
public static void Register <T, TC>()
{
Attribute [] attr = new Attribute [1];
TypeConverterAttribute vConv = new TypeConverterAttribute( typeof (TC));
attr[0] = vConv;
TypeDescriptor.AddAttributes( typeof (T), attr);
}



}
....
EditorHelper.Register <BindingExpression,BindingConvertor>();


* This source code was highlighted with Source Code Highlighter .


Everything seems to be on this, but as it turned out later - no. Binding was serialized, but the Source property did not go unnoticed at all, in the output Xaml file it was simply skipped, which led to a very limited use of such serialization. Again armed with a debager, I discovered that the serialization system determines that the property is to be serialized, based on the combination of several attributes and more crap. It seems that if it was changed, if it had a default value, etc., and tricky Microsoft, somewhere in its code, instead of returning real values, inserted “return false”, which led to ignoring the need to serialize the property. At first I was extremely upset, and it was already decided that this was the end, but after thinking I found the next solution. From the code it was clear that if the property has the attribute DefaultValue (null), then it should have been serialized. It turns out that you just need to override the type descriptor for the binding and in it replace the PropertyInfo for the Source property with the contents of the DefaultValue attribute approximately like this:

class BindingTypeDescriptionProvider : TypeDescriptionProvider
{
private static TypeDescriptionProvider defaultTypeProvider =
TypeDescriptor.GetProvider( typeof (System.Windows.Data.Binding));

public BindingTypeDescriptionProvider()
: base (defaultTypeProvider)
{
}

public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType,
object instance)
{
ICustomTypeDescriptor defaultDescriptor =
base .GetTypeDescriptor(objectType, instance);

return instance == null ? defaultDescriptor :
new BindingCustomTypeDescriptor(defaultDescriptor);
}
}

class BindingCustomTypeDescriptor : CustomTypeDescriptor
{
public BindingCustomTypeDescriptor(ICustomTypeDescriptor parent)
: base (parent)
{

}

public override PropertyDescriptorCollection GetProperties()
{

return GetProperties( new Attribute []{});
}

public override PropertyDescriptorCollection GetProperties( Attribute [] attributes)
{
PropertyDescriptorCollection pdc = new PropertyDescriptorCollection( base .GetProperties().Cast<PropertyDescriptor>().ToArray());

string [] props = { "Source" , "ValidationRules" };

foreach (PropertyDescriptor pd in props.Select(x => pdc.Find(x, false )))
{
PropertyDescriptor pd2;
pd2 = TypeDescriptor.CreateProperty( typeof (System.Windows.Data.Binding), pd, new Attribute [] { new System.ComponentModel.DefaultValueAttribute( null ), new System.ComponentModel.DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Content) });


pdc.Add(pd2);

pdc.Remove(pd);
}

return pdc;
}

}


* This source code was highlighted with Source Code Highlighter .


Well, register this type descriptor when initializing an application:
TypeDescriptor.AddProvider (new BindingTypeDescriptionProvider (), typeof (System.Windows.Data.Binding));

In short, what I did here, created my TypeDecriptorProider which for the Binding type returns a new type descriptor, in which I substitute PropertyDescriptos for the Source and ValidationRules properties. All this happens in the GetProperties () method.

All we need now to save our WPF object in XAML is to do the standard procedure for saving an object in XAML:

StringBuilder outstr= new StringBuilder ();
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true ;
settings.OmitXmlDeclaration = true ;
XamlDesignerSerializationManager dsm = new XamlDesignerSerializationManager(XmlWriter.Create(outstr,settings));
//this string need for turning on expression saving mode
dsm.XamlWriterMode = XamlWriterMode.Expression;
XamlWriter.Save(YourWWPFObj, dsm);


* This source code was highlighted with Source Code Highlighter .


Is done.
Ps. This article is a free translation with additions to my article from here. CodeProject

UPD. Thank you all, transferred to the blog WPF.

Source: https://habr.com/ru/post/86012/


All Articles