Ковыряясь в ASP.NET обнаружил вот какую штуку: оказывается, нельзя поместить MasterPage в каталог темы (App_Themes\MyTheme, например).
Хотя, казалось бы, вполне разумное решение для реализации скинов - положить все мастер-шаблоны, стили и .skin-файлы в одной папке в одном месте. Тогда и темы для приложения будет делать и деплоить легко - скопировал папочку и готово.
Ан нет. ASP.NET за этим делом следит и не дает руки распускать. А хранить шаблоны в одном месте, а темы - в другом как-то не удобно... Пришлось ковыряться глубже.
Наковырял следующее: в ASP.NET есть специальный провайдер для вычисления виртуальных путей.
Так, получается, можно сделать такой вот "финт ушами": складывать все, что касается тем, в одну папку, но не в App_Themes, а в другую, скажем, в папку Skins. А все запросы к папке App_Themes просто перенаправлять в Skins с помощью провайдера.
Забегая вперед скажу, что совсем избавиться от каталога App_Themes не удастся, придется создать его и какую-нибудь пустую папку-тему-заглушку в нем (я назвал ее Virtual). Класть туда ничего не нужно, но нужно прописать эту тему как тему по умолчанию в конфигурационном файле:
<pages validateRequest="false" theme="Virtual">
Так, делаю провайдер, наследуюсь от VirtualPathProvider. В этом провайдере переопределяю методы GetDIrectory и GetFile. Соответственно, делаю своих потомков VirtualDirectory и VirtualFile. Идея в том, чтобы VirtualDirectory представлялся системе как каталог "App_Themes/Virtual", но возвращал список файлов из другого, нужного нам каталога.
| public class ThemedVirtualPathProvider : VirtualPathProvider |
| { |
| private const string _stubTheme = "/App_Themes/Virtual/"; |
| private const string _newThemesFolder = "/Skins/"; |
| |
| public override VirtualDirectory GetDirectory(string virtualDir) |
| { |
| if (IsThemeFolder(virtualDir)) |
| return new ThemedVirtualDirectory(virtualDir); |
| return base.GetDirectory(virtualDir); |
| } |
| |
| public override VirtualFile GetFile(string virtualPath) |
| { |
| if (IsThemeFolder(virtualPath)) |
| return new ThemedVirtualFile(virtualPath); |
| return base.GetFile(virtualPath); |
| } |
| |
| internal static bool IsThemeFolder(string virtualPath) |
| { |
| return virtualPath.Contains(_stubTheme); |
| } |
| |
| internal static string ReplaceThemePath(string virtualPath) |
| { |
| virtualPath = virtualPath.Replace( |
| _stubTheme, |
| String.Concat(_newThemesFolder, SiteSettings.Instance.Theme, Path.AltDirectorySeparatorChar) |
| ); |
| return virtualPath; |
| } |
| } |
Реализация классов ThemedVirtualFile и ThemedVirtualDirectory достаточно тривиальна.
Вот пример ThemedVirtualDirectory:
| private class ThemedVirtualDirectory : VirtualDirectory |
| { |
| public ThemedVirtualDirectory(string virtualDir) |
| : base(virtualDir) |
| { |
| GetData(); |
| } |
| |
| protected void GetData() |
| { |
| string virtualDirectoryName = ThemedVirtualPathProvider.ReplaceThemePath(this.VirtualPath); |
| string directoryName = HostingEnvironment.MapPath(virtualDirectoryName); |
| if (Directory.Exists(directoryName)) |
| { |
| string tempVirtualEntry; |
| string[] fileSystemEntries = Directory.GetDirectories(directoryName); |
| ThemedVirtualFile vFile; |
| ThemedVirtualDirectory vDirectory; |
| |
| foreach (string dir in fileSystemEntries) |
| { |
| tempVirtualEntry = VirtualPathUtility.Combine(this.VirtualPath, Path.GetFileName(dir)); |
| vDirectory = new ThemedVirtualDirectory(tempVirtualEntry); |
| _children.Add(vDirectory); |
| _directories.Add(vDirectory); |
| } |
| |
| fileSystemEntries = Directory.GetFiles(directoryName); |
| foreach (string file in fileSystemEntries) |
| { |
| tempVirtualEntry = VirtualPathUtility.Combine(virtualDirectoryName, Path.GetFileName(file)); |
| vFile = new ThemedVirtualFile(tempVirtualEntry); |
| _children.Add(vFile); |
| _files.Add(vFile); |
| } |
| } |
| } |
| |
| private List<VirtualFileBase> _children = new List<VirtualFileBase>(); |
| public override IEnumerable Children |
| { |
| get { return _children; } |
| } |
| |
| private List<VirtualFileBase> _directories = new List<VirtualFileBase>(); |
| public override IEnumerable Directories |
| { |
| get { return _directories; } |
| } |
| |
| private List<VirtualFileBase> _files = new List<VirtualFileBase>(); |
| public override IEnumerable Files |
| { |
| get { return _files; } |
| } |
| } |
Здесь одна только особенность: при создании каталога ему в конструктор передается "изначальный" виртуальный путь, то есть, тот, который с "App_Themes". Потому, что ASP.NET за этим следит и если попытаться вернуть другое имя, то грязно выругается. А вот "внутри" себя он работает с нужным нам каталогом. Его подкаталоги и файлы и возвращает. А вот при создании файла ему в качестве виртуального пути передается его "реальный" путь. То есть, путь после замены "App_Themes\Virtual" на нужный нам каталог с темой. Для того, чтобы ASP.NET, вставляя, скажем, стиль в качестве ресурса страницы, ссылался все же на существующий файл.
Реализацию наследника виртуального файла я уже не привожу, должно быть все понятно. Единственное, что там нужно сделать - так это определить метод Open, который вернет поток с контентом файла..
Да, теперь как это вызывать.
Для начала провайдер нужно зарегистрировать, написав следующий вызов:
HostingEnvironment.RegisterVirtualPathProvider(new ThemedVirtualPathProvider());
Сделать это можно либо в публичном статическом методе AppInitialize() любого из классов в App_Code, либо в Global.asax в методе Application_Start, как удобнее. И в дальнейшем все вызовы будут проходить через этот провайдер.
А в базовом классе страницы в методе OnPreInit я написал следующее:
string themedMasterFile = HttpPath.Concat("~/Skins", SiteSettings.Instance.Theme, "Layout.master");
this.MasterPageFile = themedMasterFile;
То есть, тут я указываю шаблон для страницы в соответствии с темой (свойство SiteSettings.Instance.Theme возвращает имя выбранной темы), а сама тема будет подключена уже с помощью провайдера...
P.S. Поискав подобное решение в интернете я нашел лишь убогую реализацию в каком-то mojoFramework. Убогую потому, что они не предполагают, что контент в разных темах может быть разным. Потому, видимо, и не работают со списком файлов вообще. Таким образом их фреймворк позволяет в теме иметь только те, скажем, .skin-файлы, которые заданы в теме-пустышке. Только они будут "перенаправлены", об остальных система даже не узнает. Моя реализация лишена подобного недостатка ;)
Так что, надеюсь, это поможет.