четверг, 7 февраля 2008 г.

XAML localization

Начну пожалуй.

В процессе работы над проектом столкнулся с проблемой локализации, и моя святая вера в то что язык в приложении, которое собираются продавать в разных странах должен быть английским и никаким другим (что удивительно совпадает с мнением чуваков из MS =) ), разбилась. да разбилась о неколебимую твердь и т.д. и т.п.
Короче. Есть проблема и есть несколько способов ее решения. На codeproject'e есть несколько статей посвященных данной проблеме:

  1. http://www.codeproject.com/KB/WPF/WPFUsingLocbaml.aspx
  2. http://www.codeproject.com/KB/WPF/Localization_in_WPF.aspx
  3. http://www.codeproject.com/KB/WPF/WPFLocalize.aspx
  4. http://www.codeproject.com/KB/WPF/WPF-Mulit-Lingual.aspx
, а также статья в MSDN на тему WPF Globalization and Localization.

Во всех этих источниках предлагается готовое решение, но каждый из подходов обладает своими достоинствами и недостатками (с моей точки зрения), которые я вкартце перечислю:

  1. Локализация, используя resx файлы и custom code generator (его применение обусловлено ограничением binding engine wpf - binding создается только лишь для public свойств public класс'ов) (статья 1): из достоинств - поддержка design-time и легкость в использовании (всего лишь создать ObjectDataProvider натравить его на сгенерированный public класс с ресурсами и спокойно Bind'ить строки к DEPENDENCY свойствам объектов в xaml). К недостаткам прежде всего следует отнести необходимость дополнительной инсталляции к VS, и самое главное таким способом можно локализовать только dependency свойства! В других же сценариях данный способ недейственен.
  2. Локализация используя LocBaml (упрощенный вариант - создание ResourceDictionary со строками и использование их через MergedDictionaries и DynamicResource)(статья 1): урощенный да не очень. Все равно используя LocBaml сборка будет проходить в 2 этапа.
  3. Локализация используя LocBaml (полный вариант - следуя официальным руководствам от MS локализуется сам XAML, точнее BAML)(статья 1, 2): см. п.2.
  4. Подход описанный в статье №3 очень похож на 1 метод в статье 1, но отличается от него тем, что для каждого класса ресурсов автор предлагает писать класс обертку вручную, а у меня в 45 проектов и почти в каждом из них есть ресурсы! (на заметку: если использовать ссылку на файл то может быть удалось бы избежать многократного копирования, надо проверить); в design-time будут отображаться default ресурсы. Из достоинств - возможность переключать культуры в runtime.
  5. Локализация используя xml файлы, XmlDataProvider, XPath запросы в Binding(статья 4): на мой взгляд отличный способ - и поддержка design-time и runtime переключение культур, но единственный недостаток - сложность поддержания множества файлов с локализованными ресурсами.

Итак, проанализировав предложенные подходы я выделил следующие требования, которым должно удовлетворять искомое решение:

  • отсутствие ограничения на локализуемые свойства (не только dependency но и обычные clr свойства);
  • легкость локализации и поддержания ресурсов;
  • отсутсвие необходимости писать классы обертки или использовать custom tools;
  • минимальная поддержка designtime - главное чтобы UI открывался в Blend.

Слив все в один котел и немного помешав, со дна всплыло решение - markup extension.

Идея подхода следующая: все ресурсы хранятся в resx файлах, что дает нам замечательные возможности хранить в VSS, редактировать и добавлять ресурсы прямо в VS, т.к. локализация в Net 2.0 действительно была сделана великолепно; в xaml вместо строк вы подставляете этот extension и передаете ему Id строки в файле ресурсов, а extension в момент распарсивания xaml (runtime) подставляет полученную строку или сообщение об ошибке. Выглядит это примерно так:

<TextBlock Text={me:CultureResource resourceId}/>

, где me: - объявление пространства имен, в котором существует класс CultureResourceExtension. Данный подход позволяет локализовать не только dependency свойства объектов, но вообще любое строковое свойство любого объекта, создаваемого в XAML. Из недостатков же - отсутствие поддержки design-time, точнее весьма ограниченная поддержка ( все открывается в Blend, но вместо искомых строк - "ResourceStr", - что, как показала практика, некритично).

Еще из недостатков необходимость явно задавать имя класса ресурсов, в случае если:

  • имя сборки отличается от пространства имен по умолчанию;
  • файл с ресурсами не лежит в папке /Properties;
  • и то и другое вместе.

Для решения этой проблемы в класс введено свойство BaseName, в котором явно прописывается имя класса с ресурсами в виде строки. Для упрощения синтаксиса, я ввел класс BaseBinder с наследуемым по визуальному дереву attached свойством Base. С помощью него можно существенно сократить семантику записи данного класса, задавая это свойство лишь один раз корневому объектау визуального дерева. И вот тут уже будет действовать ограничение на DependencyObject - таким образом упростить запись можно только в визуальном дереве WPF, для обычных же clr объектов придется писать BaseName при каждом появлении CultureResource.

Самым сложным сценарием оказался текст форматированный с помощью модели документов WPF! Но и этот сценарий удалось решить, загружая в ресурсы не строки, а Xaml (сомнительное решение согласен, но работает). Просмотр же этого самого loose-xaml осуществляет custom control XamlViewer о котором я напишу, пожалуй, в следующий раз.

Код класса с attached свойством для упрощения записи:
public static class ResourceBaseBinder
{
public static string GetBase(DependencyObject obj)
{
return (string)obj.GetValue(BaseProperty);
}

public static void SetBase(DependencyObject obj, string value)
{
obj.SetValue(BaseProperty, value);
}

public static readonly DependencyProperty BaseProperty =
DependencyProperty.RegisterAttached("Base", typeof(string), typeof(CultureResourceExtension),
new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.Inherits));
}

Главный метод класса расширения:


Удачи.


6 комментариев:

Unknown комментирует...

прочитал перввых два предложения и последних два.
очень понравился постинг!
так держать

Анонимный комментирует...

Йо. Сначала хотел хотел написать что ничего не понял, потом подумал что твой пост не надо комментировать человеку который не разбирается в программировании (т.е. мне) :-)) А так чо, хорошее начинание! :-) Привет!

ppti комментирует...

а код markup extension приведен частично - специально?
главное идея?

RobertT комментирует...

я надеялся, что 2 закрывающие скобки вы сами поставите =)
а вообще,да. идея - самое ценное.

ppti комментирует...

да, насчет всего кода, это я погорячился. Пары скобок там конечно будет маловато, ибо есть свойство и поля, но назначения их очевидны. так что
приношу свои извинения,
статья весьма полезная.

RobertT комментирует...

в развитие идеи, взгляните на этот проект: http://wpflocalizeextension.codeplex.com/

у них есть ссылки на меня =)