Соответственно, даже обернув вызов 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 получили, пора двигать дальше.
Генерация классов для локализации
Во-первых, определимся с задачей.
Нам нужно:
- Найти все ресурсные файлы (*.resx)
- Каждый из файлов распарзить, вытащив из него двойки "название ресурса"-"комментарий"
- Для каждого файла ресурса сгенерировать отдельный класс, внутри которого сгенерировать по одному свойству на ресурс
Для демонстрации я создал тестовый проект, и добавил туда файл 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.
вмемориз :) однозначно
ОтветитьУдалитьстатья отличная, с кодогенерацией не работал, но видимо нужно начинать, вижу массу применений, в том числе и для работы с ресурсами....
было бы неплохо на будущее прикладывать сами тестовые проекты к таким статьям
Спасибо:)
ОтветитьУдалитьДа, кодогенерация штука в некоторых случаях весьма применимая.
Сам тестовый проект я б приложил с удовольствием, но делал дома, а дома у меня студия Express, и SharePoint-проект создать физически не могу:(
Поэтому приложил tt-файлик. По идее его просто добавить как Existing Item в любой проект с ресурсными файлами (но надо чтобы они лежали в отдельной папке "Resources"), дальше открываешь его и жмешь Ctrl-S - он сразу же срабатывает, и можно посмотреть сгенеренный код.
Спасибо. Очень интересная статья.
ОтветитьУдалитьКто нибудь применял этот шаблон в проекте 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
Я применяю именно в проектах SharePoint, всё нормально.
УдалитьТо что вы привели, похоже на "Preprocessed Text Template", вместо него нужно добавлять в проект элемент "Text Template".
Да, не досмотрел.Спасибо!
УдалитьА что насчет многоязычности?
ОтветитьУдалитьЕсли Вы заинтересованы в инструментов для локализации приложений, я рекомендую Вам использовать этот инструмент на базе web: https://poeditor.com/
ОтветитьУдалить