пятница, 24 сентября 2010 г.

Быстрый способ локализации Content Editor Web Part

Проблема: ContentEditorWebPart (далее CEWP) помечен как sealed, и как следствие, отнаследоваться от него мы не можем. А встроенными средствами, да, он локализацию не поддерживает. Ту, которая "на лету". По крайней мере я не нашел, как это можно сделать.

Вообще, сделать такую же вебчасть раньше не представляло совершенно никаких проблем. Например, на основе TinyMCE. Но сейчас MS реализовал классную фишку с Ribbon'ом, благодаря чему интерфейс редактора даже чем-то напоминает Word. Это выглядит очень клево, и мне нравится, и вообще это стандартная веб-часть, и здорово было бы именно её использовать.


Идея заключается в использовании ControlAdapter. Эта фишка отлично описана в блоге у Waldek Mastykarz. Вкратце, создаем класс адаптера, и затем кидаем короткий xml-файлик my.browser в папку C:\inetpub\<путь к веб-приложению>\App_Browsers.

Именно этот способ применил и я. Мой код класса адаптера выглядел следующим образом:
public class ContentEditorLocalizationAdapter : ControlAdapter
    {
        string Localizer(Match m)
        {
            return SPUtility.GetLocalizedString(
              "$Resources:" + m.Groups[2].Value,
              m.Groups[1].Value,
              (uint)System.Threading.Thread.CurrentThread.CurrentUICulture.LCID);
        }
        
        protected override void Render(System.Web.UI.HtmlTextWriter writer)
        {
            StringBuilder sb = new StringBuilder();
            HtmlTextWriter htw = new HtmlTextWriter(new StringWriter(sb));
            base.Render(htw);

            Regex regex = new Regex(
              "{{\\s*([A-Za-z0-9_\\-]+)\\s*,\\s*([A-Za-z0-9_\\-]+)\\s*}}");
            string output = regex.Replace(sb.ToString(), Localizer);

            writer.Write(output);
        }

    }
Что делает этот код: после того, как контрол полностью отрендерился, он находит все совпадения {{<имя файла ресурсов>, <имя ресурса>}}, и заменяет эти совпадения на значения соответствующих ресурсов в текущей локали сайта.

При этом всё работает как надо - т.е. "на лету", НО, если пользователь текст меняет - локализация теряется (т.к. локализация в данном случае основана на ресурсных файлах, которые в runtime менять было бы крайне неправильно, на мой взгляд). В принципе, если покопаться в Control adapter'ах, я думаю, можно найти способ полной локализации. Т.е.: переключился на русский, подредактировал русский текст, потом переключился на английский - подредактировал английский. Но, передо мной такой задачи не стояло, хотелось просто, чтобы на презентационном сайте всё выглядело круто, и начальство выделило бы на реализацию полной локализации время.

Теперь про развертывание. Итак, наш продукт поставляется в виде установщика, и совершенно обязательно все эти CEWP задеплоить на сайт клиента.

Веб-части, в том числе Content Editor Web Part, я разворачиваю на целевой сайт с помощью фич. Выглядит это примерно следующим образом:
<Module Name="Pages">
    <File Path="Pages\default.aspx" Url="Pages/default.aspx">
        <AllUsersWebPart WebPartZoneID="MainZone" WebPartOrder="1">
            <![CDATA[ 
                <WebPart xmlns="http://schemas.microsoft.com/WebPart/v2">
                    <Assembly>Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
                    <TypeName>Microsoft.SharePoint.WebPartPages.ContentEditorWebPart</TypeName>
                    <Title></Title>
                    <FrameType>None</FrameType>
                    <MissingAssembly>Cannot import this Web Part.</MissingAssembly>
                    <PartImageLarge>/_layouts/images/homepage.gif</PartImageLarge>
                    <ContentLink xmlns="http://schemas.microsoft.com/WebPart/v2/ContentEditor" />
                    <Content xmlns="http://schemas.microsoft.com/WebPart/v2/ContentEditor">
                        {{ResourceFileName, Resource1}}
                    </Content>
                    <PartStorage xmlns="http://schemas.microsoft.com/WebPart/v2/ContentEditor" />
                </WebPart>
            ]]> 
        </AllUsersWebPart>
    </File>
</Module>

Этот xml лежит в файле Elements.xml в модуле, содержащем файлик default.aspx с одной объявленной зоной веб-частей, называемой MainZone. Жирным выделен заменяемый нашим адаптером текст. Естественно, в проекте также присутствует SharePoint Mapped Folder "Resources", в который мы положили файлы ResourceFileName.resx, ResourceFileName.en-US.resx и т.п.; и в них забили ресурс с именем Resource1. Как видите, всё довольно просто.

Остается, однако, файлик my.browser, упомянутый выше. Для того, чтобы его положить в искомый App_Browsers, существует несколько ступенчатая, но зато стопроцентная технология, основывающаяся на использовании TimerJob.

Первым делом, я создаю джоб в FeatureReceiver:
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    SPSite site = properties.Feature.Parent as SPSite;

    // Check for an exising instance of the job
    foreach (SPJobDefinition job in site.WebApplication.JobDefinitions)
    {
        if (job.Name == JOB_NAME && job.WebApplication.Name == site.WebApplication.Name)
        {
            job.Delete();
        }
    }
    // Create new job
    DeployToAppBrowsersJob gsJob = new DeployToAppBrowsersJob(JOB_NAME, site.WebApplication, properties.Definition.DisplayName);
    gsJob.Title = JOB_TITLE;
    // Set up the job to run once
    gsJob.Schedule = new SPOneTimeSchedule(DateTime.Now);
    gsJob.Update();
}

Класс DeployToAppBrowsersJob, при этом, выглядит следующим образом:
public class DeployToAppBrowsersJob : SPJobDefinition
{
    [Persisted]
    private string sourcePath;

    public DeployToAppBrowsersJob() : base() { }

    public DeployToAppBrowsersJob(string jobName, SPWebApplication webApp, string featureName)
        : base(jobName, webApp, null, SPJobLockType.Job)
    {
        // Detrmine the path to the feature's resource files
        sourcePath = SPUtility.GetGenericSetupPath("Resources");    
    }

    public override void Execute(Guid targetInstanceId)
    {
        SPWebApplication webApp = this.Parent as SPWebApplication;

        foreach (SPUrlZone zone in webApp.IisSettings.Keys)
        {
            // The settings of the IIS application to update
            SPIisSettings oSettings = webApp.IisSettings[zone];

            // Determine the destination path
            string destPath = Path.Combine(oSettings.Path.ToString(), "App_Browsers");
            if (!Directory.Exists(destPath))
                Directory.CreateDirectory(destPath);

            File.Move(Path.Combine(sourcePath, "my.browser"), Path.Combine(destPath, "my.browser"));
        }
    }
}
Соответственно, здесь просто происходит вычисление путей, и последующий Move файла.
Весь код проверен и работает как часы.

Удачи!

Комментариев нет:

Отправить комментарий

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