XamlWriter and Bindings

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

    I am one of the developers of one Open Source project, one of the main parts of which is a graphical editor that must save vector graphics in XAML format as part of the WPF object model. During development, I encountered a problem. Bindings created from code (or from a loaded XAML file) are not saved back to XAML when trying to serialize with standard XamlWriter. As it turned out, this is the standard XamlWriter behavior described in MSDN. I tried to find a solution on the net, but found only one article on CodeProject. Unfortunately, as it turned out, this solution is not suitable for complex XAML documents for several reasons. I already started considering the option of writing my own serializer, when I saw that the TemplateBinding extension was perfectly preserved by standard means, it prompted me to think that not everything was lost, and armed with MS Reference Source Code and a debugger, I began to study the problem. And that’s what happened to me.


    During the debugging process, I found that when XamlWriter detects that DependencyProperty is associated with some kind of Binding, it tries to find the converter registered for the given type of binding (ExpressionConverter) which converts this binding to the MarkupExtension type. If such a converter is found, then with its help, the binding is brought to MarkupExtension which is subsequently serialized.

    Accordingly, the solution will be as follows. First, let's define the following class, inherited from ExpressionConverter, which will provide us with 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, define a static helper method and call it when the application is initialized. Like this:

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

        

    }
    ....
    EditorHelper.Register ();

    * This source code was highlighted with Source Code Highlighter.


    That seems to be all, but as it turned out later - no. Binding was serialized, but the Source property went unnoticed point-blank; it was simply skipped in the output Xaml file, which led to very limited use of such serialization. Armed again with a debager, I discovered that the serialization system determines that the property is subject to serialization, based on a combination of several attributes and any crap. It’s like whether it was changed, whether it has a default value, etc., and the cunning Microsoft, somewhere in its code, instead of returning the real values, inserted “return false”, which ignored the need to serialize the property. At first I was extremely upset, and already decided that this was the end, but after thinking it I found the following solution. The code showed that if the property has the DefaultValue (null) attribute, then it should have been serialized. It turns out that you just need to redefine the type descriptor for binding and in it replace PropertyInfo for the Source property with the contents of the DefaultValue attribute 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().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 the application:
    TypeDescriptor.AddProvider (new BindingTypeDescriptionProvider (), typeof (System.Windows.Data.Binding));

    Briefly, what I did here, I 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 now need to save our WPF object in XAML is to follow 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.


    Done.
    PS. This article is a free translation with additions to my article from here CodeProject

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

    Also popular now: