понедельник, 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-файлик (в конце поста).

среда, 16 марта 2011 г.

Sandcastle, MAML, и документирование ПО

Как известно, Visual Studio позволяет писать комментарии в коде в xml-формате, если они предваряются тройным слешем ("///"). Большинство при этом используют лишь пару простых тегов, как например <summary> и <param>.

На самом деле, тегов там можно использовать довольно много. Наиболее полезные, на которые я б сразу посоветовал обратить внимание - это <see cref="">, <example>, <para>, и <remarks>. Также, иногда очень полезен будет тег <list>. Более подробно обо всем этом ниже.

Оказывается, эти комментарии видны не только в интеллисенсе, но также на их основе можно сгенерировать CHM-файл или справочный веб-сайт, с помощью специально предназначенных для этого, общедоступных инструментов. Внешний вид получается красивый, и главное очень всем известный - всем, кто хоть раз бывал на MSDN :) Пользоваться такой справкой довольно удобно.


Мне пришлось разобраться со всем этим, когда разрабатывал описание API для одного из наших клиентов. И настолько мне понравился результат, что по собственной инициативе задокументировал некоторые неймспейсы основного нашего проекта, и сгенерировал по ним chm-файлик (он как раз представлен на скриншоте выше). Коллегам эта затея пришлась по душе, так что надеюсь в дальнейшем общими усилиями нам удастся эту линию продолжить, и создать документацию для большей части общих сборок.

В общем, в этом сообщении я расскажу немного про язык MAML, про инструменты для генерации документации (особенно про SandCastle), ну и вообще про то, как это все настраивать и как с этим всем работать.

вторник, 8 февраля 2011 г.

Программируем электронную книгу Qumo Libro

Недавно приобрел себе читалку Qumo Libro (писал об этом в Buzz'е), и был совершенно уверен, что программировать её нельзя, поскольку слово "прошивка" четко обозначало (раньше, как минимум) что-то, что заливается в ППЗУ и намертво.

И только после покупки набрел на форум сайта The-eBook.org, где обнаружилось, что писать программы для этой книжки можно, да к тому же не на какой-нибудь обрезанной Java, а на самом обычном C.

Ну, дернуло и меня этим заняться, особенно всвязи с тем, что в Libro НЕТ ШАХМАТ!! :( Но на то, чтобы подступиться к программированию, потратил кучу времени. Крупицы требуемых полезных сведений были разбросаны чуть ли не по всему форуму, и стоило большого труда всё это собрать воедино.

Между прочим, Qumo Libro программно совместима с Qumo Colibri, Digma e*, Gmini, Sibrary, и другими подобными электронными книгами, поэтому данное руководство и для вышеозначенных книг тоже должно подойти.

В общем, всвязи со всем этим, решил написать небольшую статью для тех, кто вдруг тоже купит себе читалку, совместимую с Qumo Libro, и захочет для этого девайса что-нибудь написать!

четверг, 3 февраля 2011 г.

Отображаем число подписчиков на кнопке Google buzz

В поставляемых Google'ом кнопках подписки нет возможности отобразить текущее число подписчиков.

Решив этот минус исправить, сделал себе такую вот кнопку для желающих подписаться на мой Buzz (туда транслируется данный блог, а также пощу некоторые менее формальные сообщения):


Здесь скриншот, собственно кнопка - в правой колонке на блоге (это на всякий случай, для читателей по RSS и через Buzz). Если кто-то хочет себе такую же, в этом посте объясняю, как это сделать.

среда, 2 февраля 2011 г.

Пишем Wix Extension (CompilerExtension)

Насколько мне известно, WiX - это одна из наиболее развитых и продуманных XML-систем. Причем, сделано так, что этим может пользоваться даже человек. Сегодня я еще раз убедился в том, что WiX предоставляет действительно классные возможности.

Проблема

Дело в том, что у нашего продукта множество вариаций. Версии Базовый и Стандарт, различные локализации, вариации для Saas и т.д... Все это выливается в необходимость создания большого числа однотипных установщиков.

Раньше мы использовали для этого Microsoft Setup Project, однако количество багов и слабые возможности этого решения вынудили все это переделать на WiX, чем я и занялся.

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

Поиск решения

Решение вроде бы напрашивалось само собой: в библиотеке WixUIExtension имеются элементы XmlFile и XmlConfig. Однако, для генерации больших файлов они не подходят. Попробовав их использовать, я выяснил, что 10 строк обычного xml с их помощью преобразуется аж в 100(!!).

Ну и конечно, XmlFile и XmlConfig не позволяют подключить интеллисенс, ведь для нашего файла конфигурации специально была создана тщательно документированная xsd-схема, благодаря чему создание и изменение файлов конфигурации было довольно удобным.

Поэтому, для активной генерации xml элементы XmlFile и XmlConfig явно не подходят. В процессе поиска альтернативного решения, я обратил внимание на Wix Extensions, и задумал написать собственный такой модуль расширения, который бы позволил максимально быстро и эффективно развертывать наш конкретный xml, и желательно, позволял бы использовать нашу же xsd-схему при его создании/изменении.

Например, это могло бы выглядеть примерно так:

<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">

    <!-- ... -->

    <DeployConfigFragment File="[TARGETDIR]\DeployConfig.xml" xmlns="http://www.deskwork.ru/2010/DeployConfig">
        <SolutionsList>
                <Solution Name="Softline.Portal.Helper" Id="21e394a2-7dab-4565-aaad-2540d360daf2" Require="true" Order="0" />
                <Solution Name="Softline.Portal.Libraries.MediaLibrary" Id="3b3ae4b1-26e7-4680-bd5c-db7e10de86e9" Require="true" Order="1">
                    <Features>
                        <Feature Name="Softline.Portal.Libraries.MediaLibrary.Softline_MediaLibrary" Id="fcc74650-a765-48af-91f6-247d50e7a907">
                            <ActivateRequire>true</ActivateRequire>
                        </Feature>
                    </Features>
                </Solution>
        </SolutionList>
    </DeployConfigFragment>

    <!-- ... -->

</Wix>

И все, что внутри <DeployConfigFragment>, хотелось бы чтобы в неизменном виде развертывалось в xml-файл DeployConfig.xml.

ДА! Я согласен, что это неправильно идеологически. Лучше разделять xml разного смысла, заключая inner xml в CDATA... Однако, в этом случае мы лишаемся интеллисенса, а этого бы очень не хотелось.

В общем, в итоге именно на таком варианте я и остановился, и приступил к исследованию возможности создания WiX Extensions.

Однако, в интернете оказалось не так уж и много документации по вопросу создания WiX Extension'ов, и поэтому мне пришлось лезть в исходники WiX и WiX Contrib Project.

В общем, этом посте я детально рассмотрел процесс создания CompilerExtension расширения для WiX.

понедельник, 31 января 2011 г.

Удобный способ просмотра свойств SPWeb

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

Раньше всегда использовал для этой цели консольные приложения (SharePoint Console Application), однако такой способ не всегда подходит: во-первых, у нас есть тестовые виртуалки, где студия не стоит; во-вторых, у клиентов опять же Visual Studio не установлена. Как быть?

Оказалось, ответ очень прост, и умещается в один скриншот:

SharePoint'овский PowerShell, напомню, доступен через меню Пуск. В целом, таким способом можно пробегать всю объектную модель SharePoint'а. Мне показалось очень полезным!

четверг, 20 января 2011 г.

Пустая ошибка SharePoint Designer

Столкнулись с интересной проблемой: у клиентов SharePoint Designer 2010 отказался соединяться с нашим порталом (SharePoint Foundation 2010). А сообщение об ошибке оказалось... пустым!

Симптомы (при попытке открытия сайта из SharePoint Designer):




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

Но если немножко поискать, в Windows Event Viewer можно обнаружить более подробное описание данной ошибки. Впрочем, тоже не слишком вразумительное:

Службе WebHost не удалось обработать запрос
Сведения об отправителе: System.ServiceModel.ServiceHostingEnvironment+HostingManager/32521977
Исключение: System.ServiceModel.ServiceActivationException: Не удается активировать запрошенную службу "/_vti_bin/client.svc" из-за исключения во время компиляции. Сообщение об исключении: Операция является недопустимой из-за текущего состояния объекта.. ---> System.InvalidOperationException: Операция является недопустимой из-за текущего состояния объекта.
в Microsoft.SharePoint.ApplicationRuntime.SPLayoutsMappedFile.MapLayoutsVirtualPath(String virtualPath)
в Microsoft.SharePoint.ApplicationRuntime.SPVirtualPathProvider.GetCacheKey(String virtualPath)
в System.Web.Compilation.BuildManager.GetCacheKeyFromVirtualPath(VirtualPath virtualPath, Boolean& keyFromVPP)
в System.Web.Compilation.BuildManager.GetVPathBuildResultInternal(VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile)
в System.Web.Compilation.BuildManager.GetVPathBuildResultWithNoAssert(HttpContext context, VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile)
в System.Web.Compilation.BuildManager.GetCompiledCustomString(String virtualPath)
в System.ServiceModel.ServiceHostingEnvironment.HostingManager.GetCompiledCustomString(String normalizedVirtualPath)
в System.ServiceModel.ServiceHostingEnvironment.HostingManager.CreateService(String normalizedVirtualPath)
в System.ServiceModel.ServiceHostingEnvironment.HostingManager.ActivateService(String normalizedVirtualPath)
в System.ServiceModel.ServiceHostingEnvironment.HostingManager.EnsureServiceAvailable(String normalizedVirtualPath)
--- Конец трассировки внутреннего стека исключений ---
в System.ServiceModel.ServiceHostingEnvironment.HostingManager.EnsureServiceAvailable(String normalizedVirtualPath)
в System.ServiceModel.ServiceHostingEnvironment.EnsureServiceAvailableFast(String relativeVirtualPath)
Имя процесса: w3wp

На системах с английской локалью сообщение выглядит следующим образом:


WebHost failed to process a request.
Sender Information: System.ServiceModel.ServiceHostingEnvironment+HostingManager/17653682
Exception: System.ServiceModel.ServiceActivationException: The service '/_vti_bin/client.svc' cannot be activated due to an exception during compilation. The exception message is: Operation is not valid due to the current state of the object.. ---> System.InvalidOperationException: Operation is not valid due to the current state of the object.
at Microsoft.SharePoint.ApplicationRuntime.SPLayoutsMappedFile.MapLayoutsVirtualPath(String virtualPath)
at Microsoft.SharePoint.ApplicationRuntime.SPVirtualPathProvider.GetCacheKey(String virtualPath)
at Microsoft.SharePoint.Publishing.Internal.CmsVirtualPathProvider.GetCacheKey(String virtualPath)
at System.Web.Compilation.BuildManager.GetCacheKeyFromVirtualPath(VirtualPath virtualPath, Boolean& keyFromVPP)
at System.Web.Compilation.BuildManager.GetVPathBuildResultInternal(VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile)
at System.Web.Compilation.BuildManager.GetVPathBuildResultWithNoAssert(HttpContext context, VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile)
at System.Web.Compilation.BuildManager.GetCompiledCustomString(String virtualPath)
at System.ServiceModel.ServiceHostingEnvironment.HostingManager.GetCompiledCustomString(String normalizedVirtualPath)
at System.ServiceModel.ServiceHostingEnvironment.HostingManager.CreateService(String normalizedVirtualPath)
at System.ServiceModel.ServiceHostingEnvironment.HostingManager.ActivateService(String normalizedVirtualPath)
at System.ServiceModel.ServiceHostingEnvironment.HostingManager.EnsureServiceAvailable(String normalizedVirtualPath)
--- End of inner exception stack trace ---
at System.ServiceModel.ServiceHostingEnvironment.HostingManager.EnsureServiceAvailable(String normalizedVirtualPath)
at System.ServiceModel.ServiceHostingEnvironment.EnsureServiceAvailableFast(String relativeVirtualPath)
Process Name: w3wp


Ниже описано решение данной проблемы.