понедельник, 21 марта 2011 г.

Локализация SharePoint: кодогенерация ресурсных классов

SharePoint обладает одной довольно неприятной особенностью в плане локализации: он плохо дружит со стандартными средствами ASP.Net в этой области. Фактически, в SharePoint-проектах желательно везде использовать локализацию через SPUtility.GetLocalizedString, причем в качестве параметра language нужно обязательно передавать свойство LCID объекта Thread.CurrentThread.CurrentUICulture (чтобы работала on-fly локализация).

Соответственно, даже обернув вызов GetLocalizedString в какие-то минимальные врапперы, все равно от нехороших конструкций вида LocalizationHelper.Localize("ResourceName", "ResourceFile") уйти не удастся. Основная проблема - это, конечно, наличие в таком коде большого количества "magic strings", со всеми вытекающими.

Частично от этого спасает введение класса, к примеру, ResourceNames, который хранит константы для всех названий ресурсов. Однако, в этом случае приходится содержать целый лишний класс, поддерживать его, да и от magic strings мы таким образом не избавляемся, а просто держим их отдельно.

Достаточно долгое время наш основной рабочий проект работал именно по приведенной выше схеме с magic strings. Причем, когда я (еще летом 2010) исследовал возможности и средства локализации проекта, естественно искал в интернетах - но никаких других толковых решений не нашел.

И вот один мой коллега реализовал другое решение - на основе кодогенерации. С кодогенерацией я работал очень мало, по сути только полгода назад узнал о том, что это такое. Но идея-то очень логичная, и совершенно изумительная в плане результата: полностью избавляемся от magic strings, и получаем все преимущества интеллисенса.

В результате применения кодогенерации, становится возможным использовать (причем, Intellisense при этом отлично работает) конструкции вида: Resources.ResourceFileName.ResourceName. Для нашего основного проекта я сделал еще интереснее. Поскольку ресурсов там очень много, и все именуются по стандартному принципу, мне удалось сделать бОльшую вложенность классов, и за счет этого упростить выбор ресурса. Т.е. у нас получилось что-то вроде такого: Resources.ResourceFileName.ProjectName.ModuleName.ResourceName.

Более того, мне также удалось добиться отображения комментариев (колонка Comment в файле resx) через Intellisense! Выглядит все это весьма эффектно:


В общем, в этом посте я расскажу про особенности реализации, те же кому важен конечный результат, могут стянуть сразу tt-файлик (в конце поста).

Шаблоны T4

Те кто уже знает, что такое T4, могут эту часть поста пропустить.

Visual Studio позволяет реализовать кодогенерацию несколькими способами, один из которых - T4-шаблоны. Именно этот способ выбрал и я.

Шаблонами они называются не просто так. Фактически, T4-шаблон представляет собой файл с шаблоном кода, и с помощью специально оформленных вставок можно добавить в этот шаблон интерактивность. Ну например, шаблон:

public class FiveProperties
{
    <#
    for (int i = 1; i <= 5; i++)
    {
    #>
        public int P<#= i #> { get; set; }
    <#
    }
    #>
}

Сгенерирует на выходе файл:

public class FiveProperties
{
        public int P1 { get; set; }
        public int P2 { get; set; }
        public int P3 { get; set; }
        public int P4 { get; set; }
        public int P5 { get; set; }
}

Основной плюс T4 - в том, что он уже работает в вашей студии. Даже в моей домашней Express. Он уже туда встроен, и ничего в систему дополнительно устанавливать не нужно. Это большой плюс, поскольку мне кажется, что проект взятый из SVN должен работать без каких-либо дополнительных телодвижений (или с минимумом их).

Правда, есть и минус. Почему-то интеллисенс для T4 только внешний (т.е. нужно ставить плагин), но с другой стороны шаблоны пишутся нечасто, и изменять их постоянно не придется. Кроме того, шаблоны чаще всего очень простые, и лично я, к примеру, вообще обошелся без интеллисенса.

Я сам впервые услышал про T4 от Хансельмана. Он рассказывал про ASP.Net MVC приложение, в котором удалось избавиться от magic strings в Html.ActionLink. Т.е., к примеру:

<%= Html.ActionLink("Delete Dinner", "Delete", new { id = Model.DinnerID }) %>

Заменяется на:

<%= Html.ActionLink("Delete Dinner", MVC.Dinners.Delete(Model.DinnerID)) %>

Если вам это интересно, рекомендую почитать статью Дэвида Эббо (David Ebbo) "A new and improved ASP.NET MVC T4 template".

Ну вот, базовые знания о T4 получили, пора двигать дальше.

Генерация классов для локализации

Во-первых, определимся с задачей.
Нам нужно:
  1. Найти все ресурсные файлы (*.resx)
  2. Каждый из файлов распарзить, вытащив из него двойки "название ресурса"-"комментарий"
  3. Для каждого файла ресурса сгенерировать отдельный класс, внутри которого сгенерировать по одному свойству на ресурс 
Да, и при этом, часто хочется, чтобы не было необходимости пихать T4-шаблон в каждый из проектов солюшена.

Для демонстрации я создал тестовый проект, и добавил туда файл T4-шаблона (с расширением tt). Также в проект я добавил файл ресурсов, который поместил в отдельную папку Resources. В случае SharePoint-проекта эта папка должна быть Mapped SharePoint Folder, указывающая на каталог 14\Resoures, но дома у меня студия Express, поэтому у меня получилось вот так:

В файл ресурсов я добавил одну строку:


Вообще говоря, ресурсные файлы представляют собой обычный xml-файлик. Если нажать в студии правой кнопкой по ресурсному файлу, выбрать Open With... -> XML (Text) editor, увидим его содержимое. В начале файла куча ненужного: комментарии с описанием формата, xsd-схема, заголовочный XML. И лишь промотав в самый конец файла, можно увидеть нужный нам XML:

<data name="TestResource" xml:space="preserve">
  <value>Test resource value</value>
  <comment>Hello from resx comments!</comment>
</data>

Для разбора такого XML я использовал следующий код:

var resources = XElement
    .Load(fileName)
    .Elements("data")
    .ToDictionary(
     e => e.Attribute("name").Value, 
     e => e.Element("comment") == null ? String.Empty : e.Element("comment").Value);

Осталось лишь обойти все каталоги проектов, и найти resx-файлы. Это делается довольно просто (при этом, я предполагаю, что внутри каждого проекта создан SharePoint Mapped Folder "Resources", в котором и лежат resx-ы):

DirectoryInfo solutionDirectory = Directory.GetParent(Path.GetDirectoryName(this.Host.TemplateFile));
foreach (string dir in solutionDirectory.GetDirectories().Select(d => d.FullName))
{
  string path = Path.Combine(dir, "Resources");
  if (!Directory.Exists(path))
   continue;

  foreach (string fileName in Directory.GetFiles(path, "*.resx"))
  {
    string className = Path.GetFileNameWithoutExtension(fileName);

    // Отсекаем файлы *.ru-RU.resx, *.en-US.resx и т.п.; генерация производится только для дефолтных файлов ресурсов
    if (className.Contains("."))
     continue;

    // ...
    // здесь разбор ресурсов и генерация кода
    // ...

}

Собственно, из каких-то "нестандартных" вещей здесь используется только this.Host.TemplateFile - это путь к нашему файлу Resources.tt.

Оставшаяся часть кодогенерации делается уж совсем элементарно.

В результате, у меня сгенерировался следующий код:

//
// Этот код был сгенерирован автоматически! Не изменяйте его.
// Дата генерации: 03/20/2011 11:01:32
//

namespace ResourcesTest
{
 using System;
 using System.Threading;
 using System.CodeDom.Compiler;
 using Microsoft.SharePoint.Utilities;

 /// <summary>
 /// Класс для прямого обращения к ресурсам.
 /// </summary>
 /// <remarks>
 /// Для создания этого и вложенных классов используется кодогенерация на основе T4-шаблона (файл Resources.tt).
 /// При этом обрабатываются файлы ресурсов всех проектов решения, по маске: "{Project name}\Resources\*.resx".
 /// </remarks>
 public static class Resources
 {

   /// <summary>
   /// Resources from file Resource1.resx.
   /// </summary>
   [GeneratedCodeAttribute("Resources.tt", "1.0.0.0")]
   public static class Resource1
   {

    /// <summary>
    /// Hello from resx comments!
    /// </summary>
    public static string TestResource 
    { 
     get 
     { 
      return SPUtility.GetLocalizedString(
       "$Resources: TestResource", 
       "Resource1", 
       Thread.CurrentThread.CurrentUICulture.LCID); 
     }
    }

   }

 }

}

На всякий случай, я выложил полный код tt-файла, который вы можете без изменений использовать в собственных проектах.

Вот и все! Как видите, все очень легко.

Другие возможности для применения

Для SharePoint сразу же приходят в голову и другие интересные варианты использования кодогенерации. Ну например, можно натравить T4-шаблоны на SharePoint Mapped Folders, чтобы все ссылки на картинки, ресурсы, контролы, страницы избавить от Magic Strings.

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

  1. вмемориз :) однозначно
    статья отличная, с кодогенерацией не работал, но видимо нужно начинать, вижу массу применений, в том числе и для работы с ресурсами....

    было бы неплохо на будущее прикладывать сами тестовые проекты к таким статьям

    ОтветитьУдалить
  2. Спасибо:)
    Да, кодогенерация штука в некоторых случаях весьма применимая.

    Сам тестовый проект я б приложил с удовольствием, но делал дома, а дома у меня студия Express, и SharePoint-проект создать физически не могу:(

    Поэтому приложил tt-файлик. По идее его просто добавить как Existing Item в любой проект с ресурсными файлами (но надо чтобы они лежали в отдельной папке "Resources"), дальше открываешь его и жмешь Ctrl-S - он сразу же срабатывает, и можно посмотреть сгенеренный код.

    ОтветитьУдалить
  3. Спасибо. Очень интересная статья.

    ОтветитьУдалить
  4. Кто нибудь применял этот шаблон в проекте SharePoint? В сгененрированном файле получается такой кусок кода:
    private global::Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost hostValue;
    public virtual global::Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost Host
    {
    get
    {
    return this.hostValue;
    }
    set
    {
    this.hostValue = value;
    }
    }
    Но сборка Microsoft.VisualStudio.TextTemplating, доступна только для версии .net 4.0. И то только после установки VS SP1 SDK. А как известно для SharePoint нужно не выше 3.5

    ОтветитьУдалить
    Ответы
    1. Я применяю именно в проектах SharePoint, всё нормально.
      То что вы привели, похоже на "Preprocessed Text Template", вместо него нужно добавлять в проект элемент "Text Template".

      Удалить
    2. Да, не досмотрел.Спасибо!

      Удалить
  5. А что насчет многоязычности?

    ОтветитьУдалить
  6. Если Вы заинтересованы в инструментов для локализации приложений, я рекомендую Вам использовать этот инструмент на базе web: https://poeditor.com/

    ОтветитьУдалить

Внимание! Реклама и прочий спам будут беспощадно удаляться.