Как стать автором
Обновить

XamlWriter и Bindings

Время на прочтение6 мин
Количество просмотров2.6K
Доброй ночи Хабра-сообщество.
Я только что получил инвайт к вам в компанию, и сразу же решил написать что-то, что возможно окажется для кого-то полезным… Не судите строго.

Я являюсь одним из разработчиков одного Open Source проекта, одной из основных частей которого является графический редактор, который должен сохранять векторную графику в формате XAML в рамках объектной модели WPF. В процессе разработки, я столкнулся с проблемой. Bindings, созданные из кода ,(или из загруженного XAML файла) не сохраняется обратно в XAML при попытке сериализации стандартным XamlWriter. Как оказалось это стандартное поведение XamlWriter описанное в MSDN. Я пытался найти решение в сети, но нашёл только одну статью на CodeProject. К сожалению, как оказалось, это решение не подходит для сложных XAML документов по ряду причин. Я уже начал рассматривать вариант написания собственного сериализатора, когда увидел, что расширение TemplateBinding прекрасно сохраняется стандартными средствами, это меня натолкнуло на мысль, что ещё не всё потеряно, и вооружившись Reference Source Code от MS и дебагером я начал изучать проблему. И вот что у меня вышло.


В процессе дебага я обнаружил, что когда XamlWriter обнаруживает, что DependencyProperty связано с каким то Binding, то он пытается найти зарегистрированный для заданного типа байндинга конвертор(ExpressionConverter) который приводит этот байндинг к типу MarkupExtension. Если такой конвертор найден то с его помощью байндинг приводится к MarkupExtension который в дальнейшем сериализуется.

Соответственно решение будет следующим. Для начала определим следующий класс, унаследованный от ExpressionConverter который обеспечит нам конверсию из Binding в 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.


После этого этот конвертор необходимо зарегистрировать. Для этого определим статический метод-хелпер и вызовем его при инициализации приложения. Вот так:

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.


На этом вроде бы всё, но как оказалось в последствии — нет. Байндинг сериализовался но, свойство Source в упор оставалось незамеченным, в выходном Xaml файле оно просто пропускалось, что приводило к очень ограниченному использованию такой сериализации. Опять вооружившись дебагерром я обнаружил, что система сериализации, определяет, что свойство подлежит сериализации, исходя из совокупности нескольких атрибутов и ещё всякой лабуды. Вроде того, было ли оно изменено, имеет ли дефолтное значение итд, и хитрый Майкрософт, где-то в своём коде, вместо возврата реальных значений вставил «return false» что приводило к игнорированию необходимости сериализации свойства. Сначала я был крайне огорчён, и уже было решил что это конец, но подумав нашёл следующее решение. По коду было видно, что если свойство имеет атрибут DefaultValue(null) то оно должно было сериализоваться. Получается, что необходимо просто переопределить дескриптор типа для байндинга и в нём подменить PropertyInfo для свойства Source с содержанием атрибута DefaultValue приблизительно вот так:

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.


Ну и зарегистрировать этот дескриптор типа при инициализации приложения:
TypeDescriptor.AddProvider(new BindingTypeDescriptionProvider(),typeof(System.Windows.Data.Binding));

Коротко, что я тут сделал, создал свой TypeDecriptorProider который для типа Binding возвращает новый дескриптор типа, в котором я подменяю PropertyDescriptos для свойств Source и ValidationRules. Всё это происходит в методе GetProperties().

Всё что нам теперь нужно для сохранения нашего WPF объекта в XAML это проделать стандартную процедуру сохранения объекта в 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.


Готово.
PS. Эта статья — вольный перевод с дополнениями моей же статьи отсюда СodeProject

UPD. Всем спасибо, перенёс в блог WPF.
Теги:
Хабы:
+23
Комментарии22

Публикации

Изменить настройки темы

Истории

Работа

.NET разработчик
74 вакансии

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн