In this article I’d like to describe new Episerver extension for converting pages in edit mode. It can be useful for Editors to simplify updates of site structure and keeps links to the content.
Converting pages in admin mode
In Admin mode, there is a Convert Pages tool that allows to convert pages in tree structure from one page type into another. Administrator sets the source page (or the root page when converting together with subpages), source and target page types and property mappings. Any properties with matching types can be converted. Properties that are set to “Remove property permanently” won’t be available after the conversion.
The tool is flexible and allows to convert any page types. It can be especially useful during upgrading the site when existing content has to be migrated.
Converting pages in edit mode
The Convert Pages tool is not available for Edit Mode. Converting one page type into another has to be a conscious decision, because of the possibility of data loss. But is it always risky?
Below is the structure of page models in Alloy demo site.
Converting between ContactPage and StartPage won’t be very useful for Editors, but Article, News and Product share same base class. For example we could have a StandardPage that should be converted to ArticlePage. Because ArticlePage inherits from StandardPage, the StandardPage properties is a subset of ArticlePage properties and there will be no data loss.
What is the benefit of converting pages instead of creating new page instance with content copy? First of all Editor has to create new page and manually copy all properties from one page to another. But also, if the page is referenced by other pages, those references will be lost when deleting old page. Those references have to be added manually.
When converting a page, the ContentLink won’t change and all links will be available after the conversion.
Converting pages extension
When using my extenstion Editor has limited option to convert pages in Edit Mode. There is no bulk conversion, only one page can be converted at time. All available conversions has to be registered using PageConversionSettingsRepository. The repository stores mappings for page types. Every mapping has:
- Source page type id
- Target page type id
- Property mappings – list of KeyValue pairs containing source property id and target property id
For one page type there could be more than one conversion registered. For example, for StandardPage we could have conversions to ArticlePage, NewsPage and ProductPage.
Operating on IDs when configuring page types and properties is a bit inconvenient to use, so I implemented a builder that simplify creating mappings. Builder is a generic class and use property expressions to add property mappings. For example to create a mapping from StandardPage to ArticlePage you can create builder: new PropertyMappingsBuilder<StandardPage,ArticlePage>(contentTypeRepository) and then add properties using AddMapping method. AddMapping has two parameters: expression for source property and expression for target property:
1 2 3 4 5 |
var standardToArticle = new PropertyMappingsBuilder<StandardPage,ArticlePage>(contentTypeRepository) .AddMapping(x=>x.MainBody, x=>x.MainBody) .AddMapping(x=>x.MainContentArea, x=>x.MainContentArea) /*all other properties*/ .Build(); |
But in most cases (like in example above), for simple edit mode conversions all property names will match. That’s why builder has also “AddAllWithSameNameAnType” method that automatically maps all properties that has same name and type.
Of course when converting from Article to StandardPage properties from Article that can’t be mapped will be lost.
The result of “Build” method should be saved in repository. To register mappings we should use ModuleInitializer. The full example of mapping between StandardPage and base classes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))] public class PageConversionMappingsInitialization : IInitializableModule { public void Initialize(InitializationEngine context) { var contentTypeRepository = ServiceLocator.Current.GetInstance<IContentTypeRepository>(); var pageConversionSettingsRepository = ServiceLocator.Current.GetInstance<PageConversionSettingsRepository>(); // StandardPage: NewsPage, ArticlePage, ProductPage var standardToArticle = new PropertyMappingsBuilder<StandardPage,ArticlePage>(contentTypeRepository).AddAllWithSameNameAnType(); var standardToProduct = new PropertyMappingsBuilder<StandardPage,ProductPage>(contentTypeRepository).AddAllWithSameNameAnType(); var standardToNews = new PropertyMappingsBuilder<StandardPage,NewsPage>(contentTypeRepository).AddAllWithSameNameAnType(); // ArticlePage: NewsPage var articleToNews = new PropertyMappingsBuilder<ArticlePage, NewsPage>(contentTypeRepository).AddAllWithSameNameAnType(); pageConversionSettingsRepository.Save(standardToArticle.Build()); pageConversionSettingsRepository.Save(standardToProduct.Build()); pageConversionSettingsRepository.Save(standardToNews.Build()); pageConversionSettingsRepository.Save(articleToNews.Build()); } public void Uninitialize(InitializationEngine context) { } public void Preload(string[] parameters) { } } |
When fields are not match we can use “AddMapping” method and convert one property into another. For example I have ContactPage with two subclasses EmployeePage and CustomerPage. When converting from Employee to Customer, the “EmployeeId” property from EmployeePage should be copied to “CustomerId” property from CustomerPage. When converting from Customer to Employee, the “CustomerId” from CustomerPage should be copied to “AdditionalInfo” on EmployeePage.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
[SiteContentType(GUID = "F8D47655-7B50-4319-8646-3369BA9AF05B")] public class ContactPage : SitePageData { [UIHint(UIHint.Image)] public virtual ContentReference Image { get; set; } public virtual string Email { get; set; } } [SiteContentType(GUID = "B844E5C8-0117-45CD-9593-4FDDC2AF693D")] public class EmployeePage : ContactPage { public virtual string EmployeeId { get; set; } public virtual string AdditionalInfo { get; set; } } [SiteContentType(GUID = "5CF1F9F4-0DB5-4F26-913F-C4D8C9829617")] public class CustomerPage : ContactPage { public virtual string CustomerId { get; set; } } [ModuleDependency(typeof(EPiServer.Web.InitializationModule))] public class PageConversionMappingsInitialization : IInitializableModule { public void Initialize(InitializationEngine context) { var employeeToCustomer = new PropertyMappingsBuilder<EmployeePage, CustomerPage>(contentTypeRepository) .AddAllWithSameNameAnType() .AddMapping(x=>x.EmployeeId, x=>x.CustomerId); var customerToEmployee = new PropertyMappingsBuilder<CustomerPage, EmployeePage>(contentTypeRepository) .AddAllWithSameNameAnType() .AddMapping(x=>x.CustomerId, x=>x.AdditionalInfo); pageConversionSettingsRepository.Save(employeeToCustomer.Build()); pageConversionSettingsRepository.Save(customerToEmployee.Build()); } public void Uninitialize(InitializationEngine context) { } public void Preload(string[] parameters) { } } |
Conversions are located under Page Tools commands. When only one conversion for page is available, like Article can be converted only to News, then single menu option is created.
When current page has many conversions, like StandardPage can be converted to Article, News and Product, then submenu is displayed.
Running the conversion command will use the same API as the admin mode tool.
Below is a demo of converting StandardPage to ArticlePage:
The source code together with working example is available on Github (edit-mode-page-convert branch).