Page Type Builder 2 Preview 2

A few weeks ago we released the first preview of Page Type Builder 2. This weekend a second preview was released. While the first preview offered new and exciting functionality with support for property settings and a new concept called property groups this version has it’s aim focused at addressing some of the limitations to the functionality that Page Type Builder has. It does this in two ways, with a new project called PageTypeBuilder.Migrations and by automatically changing the type of properties in some situations.

Background – Limitations to the synchronization of page types with the code

While Page Type Builder supports adding page types and page definitions (properties on page types in EPiServer) based on classes and properties in code it doesn't support removing the same. It’s ability to update existing page types and page definitions has also been somewhat limited. It only renames page types if they are mapped using the page type’s GUID and it doesn’t rename or change the type of page definitions.

These limitations exist because Page Type Builder has to use names to identify page types and page definitions and because EPiServer permanently deletes data when we change the type of a property. While there have been many discussions about these limitations as well as ideas for working around them none has been bullet proof. And considering the risk of users of Page Type Builder losing data they haven’t been implemented.

One idea that seemed very promising though was brought up by Lee Crowe in a comment to the post about the previous preview release; automatically change the type of properties if their value types are compatible. After some investigation I found that this exact idea wouldn’t work out of the box, but with some modifications.

Automatic updating of some page definition types

The reason it wouldn’t work out of the box is that even though the value type of property types are the same they can be stored in different ways in the database. For instance both a property of type PropertyString  and a property of type PropertyLongString have string as value type but the PropertyString will be stored in a column that is limited to 255 characters.

What would be safe though would be to automatically change the type of a page definition (an EPiServer property on a page type) if the old and new type have the same data type. The data type is an enum (PropertyDataType) and all property types (descendants of PropertyData) must specify which data type it maps to to use using it’s Type property. When EPiServer saves an instance of the property it uses its data type to determine what type of column it should be stored in in the database. In other words, as long as the old and the new property types have the same data type changing the type of a page definition should never lead to data being lost.

Therefore, in this release Page Type Builder will automatically update the types of properties if the old and the new type have the same data type. This for instance means that Page Type Builder will automatically change the type to and from PropertyLongString and PropertyXhtmlString. It will however not update to and from PropertyString and PropertyXhtmlString as they have different data types.

Migrations

Another idea for addressing the limitations of Page Type Builder that has been discussed for a while has been to introduce functionality to explicitly remove and rename page types and page definitions. Something similar to the many migration frameworks that exists for databases, such as Rails Migrations Migrator.NET.

Migration frameworks usually allow developers to create individual migrations that contain three things: a number (or a timestamp), a method for applying the migration (often named Up) and a method for undoing the migration (often named Down). In the methods the developer can add new tables or columns, rename existing database entities and move data around. The frameworks keep track of which migrations have been previously applied and executes any new ones.

This often of works great for databases but it isn’t a perfect fit for Page Type Builder as it already addresses adding new things and doesn’t communicate directly with the database, thus making moving data around so as to not lose it harder. The general concept however should work. Therefore this version of Page Type Builder ships with a new project/assembly named PageTypeBuilder.Migrations. It differs somewhat from regular migration frameworks as it only has a method for applying the migration and no method for undoing it.

While this new assembly ships with Page Type Builder using it is fully optional. The core Page Type Builder assembly doesn’t require it. And while it gives you power to easily remove or modify things without going into admin mode you should remember that with power comes responsibility. In other words – it should be used with some caution.

Using Migrations

In order to use the migrations project you need to have the assembly loaded in your application domain (reference it in your project) along with the regular Page Type Builder assembly. Once you have that you create migrations by creating classes that implement the IMigration interface, which looks like this:

namespace PageTypeBuilder.Migrations
{
    public interface IMigration
    {
        void Execute();
    }
}

As you can see it has a single method named Execute which has to be implemented. This method will be invoked just before Page Type Builder does its regular synchronization.

In addition to implementing the IMigration interface there are a number of strict requirements for migration classes:

  • They have to be named MigrationX where X is an integer larger than 0. X also cannot start with 0, so Migration1 is a valid name while Migration01 is not.
  • All migrations have to reside in the same namespace.
  • All migrations must be numbered in a consecutive sequence. That is, you cannot have Migration1 and Migration3 without having Migration2.
  • All previously applied migrations must be present in the application domain. In other words, you cannot remove old migrations. The history of applied migrations is stored in the database using DDS.

If any of the above requirements aren’t met an exception will be thrown and further execution will be halted until the problem has been addressed or migrations have been disabled.

These requirements may seem strict but they are there to help us prevent problems that we may otherwise run into. For instance, the strict naming standard and the requirement for all migrations to be in the same namespace is there to prevent two developers from both checking in a migration with the same number into source control. The requirement for all migrations to be present in the application domain is there to address the situation where two developers use the same database and one of them has removed something while the other still has an old version of the source code, effectively re-adding whatever the first developer just removed.

The Migration class

While the framework will treat any non-abstract class that implements the IMigration interface as a migration it also ships with an abstract class named Migration that we can inherit from. By doing so we gain access to some helper classes and methods that make common tasks easier to do. For instance, if we wanted to create a migration that changed the type of the a property named Preamble in a page type named Article we could implement it like this:

using EPiServer.Core;
using PageTypeBuilder.Migrations;

public class Migration1 : Migration
{
    public override void Execute()
    {
        PageType("Article")
            .PageDefinition("Preamble")
                .ChangeTypeTo<PropertyLongString>();
    }
}

The helper methods, such as PageType, PageDefinition and ChangeTypeTo illustrated above, all share two conceptual characteristics. First they make it easy to implement common tasks in migrations. Second they do it in a forgiving way. For instance, if there isn’t any page type named Article the above code wouldn’t throw an exception. Execution would just pass through it and it would log that the migration tried to retrieve a page type with that name but failed to find one.

I think the forgiving nature of the helpers is good in scenarios where we’re deploying to a production environment. It might however cause some annoyance during development as we won’t get any feedback as to why a migration didn’t work (except for in the log) and we would have to create a new migration or clear the log of applied migrations in the database. This is something I plan to play with a bit myself and definitely would appreciate feedback about.

Below is further examples of the helpers that ships with the current version.

PageType("Article").Delete();

PageType("Article").Rename("Chronicle");

PageType("Article")
    .PageDefinition("Preamble")
        .Delete();

PageType("Article")
    .PageDefinition("Preamble")
        .Rename("MainIntro");

PageType("Article")
    .PageDefinition("Preamble")
        .ChangeTypeTo<PropertyXhtmlString>();

Disabling migrations through configuration

While I wouldn’t recommend switching back and forth from using migrations in development there might be times when we are forced to, such as if another developer with whom we share the database has created a migration and went home without checking it in (in this scenario I would advice to temporarily create dummy migration with the same number though). However, for production environments with multiple web servers it might be advisable to disable migrations on all but one web server.

Anyhow, to disable migrations from being both validated and applied at startup we can add a new configuration section to web.config and set the disabled attribute to true, like this:

<configuration>
  <configSections> 
    <section name="pagetypebuilder.migrations" 
        type="PageTypeBuilder.Migrations.Settings, 
            PageTypeBuilder.Migrations"/>
  </configSections>
  <pagetypebuilder.migrations disabled="true" />
</configuration>

Other changes

Apart from the changes and new functionality described above there has been quite a few changes internally to Page Type Builder. If you download the release you will also find that it has a new dependency to the IoC container Autofac. While I haven’t tested it this release should be backwards compatible with previous releases though, with one exception: properties in interfaces annotated with the PageTypeProperty attribute will no longer be mapped to properties on page types unless of course the implementing class also annotates the property with the PageTypeProperty attribute. Allowing (EPiServer) property declarations in interfaces, which was introduced in version 1.3, demanded quite a lot of complexity, especially for validation and seeing how the new concept of property groups made the functionality redundant I decided to scrap it.

Feedback

While I don’t consider this release stable that’s primarily because I haven’t used it myself on a real site yet or gone through the usual pre-release “just-to-be-sure-profiling”. The code is however covered by an extensive set of tests and executable specifications (from which the output is shipped with the release in the specifications.html file). Therefore, I wouldn’t hesitate to use it myself if I was working on a project that wasn’t about to go live very soon and I would very much appreciate it if anyone working on such a project would give it a try. Feedback from real world usage is the only way to guarantee a nice API and true stability.

Even if you don’t use this new release I would very much appreciate any and all feedback though. Especially as the changes made in this release are somewhat experimental and I’m sure that they can be further improved.

PS. For updates about new posts, sites I find useful and the occasional rant you can follow me on Twitter. You are also most welcome to subscribe to the RSS-feed.

Comments

  1. Lee Crowe's avatar

    Lee Crowe 11 months ago

    Excellent work as always, it's good to see you added the changes to change the property types if the data type was the same. I didn't think about values being stored in different columns ;)

    The migration stuff looks pretty cool too, I look forward to having a play with it when I get chance.

    Nice one, PTB rocks :)

  2. Jarle Friestad's avatar

    Jarle Friestad 11 months ago

    This looks very good :-)

  3. Erik Nordin's avatar

    Erik Nordin 11 months ago

    This looks nice!

    Have you put any thought how you could change for example "Preamble" for all page types?

    Could be nice since some properties probably are inherited by all page types.

    Keep up the great work! :)

  4. Joel Abrahamsson's avatar

    Joel Abrahamsson 11 months ago

    Erik, yeah, the only problem there is that since migrations are executed before Page Type Builder has done its synchronization they cannot use PageTypeResolver.Instance meaning we don't really know about the inheritance relationships. But I'm sure we can figure something out :)

  5. Erik Nordin's avatar

    Erik Nordin 11 months ago

    I see! Since you probably won't use this on a day to day basis I guess you could live with some copy paste. But as said, would be nice. :)

  6. Lars Kolsaker's avatar

    Lars Kolsaker 11 months ago

    Great work as always, Joel!
    I am using PageTypeBuilder with Composer using ComposerBuilder. After upgrading to PTB 2 we did have some problem with getting this to work.
    It boils down to that you in the new version are returning WrappedPageType from methods in PageRepository.
    To integrate with Composer we need to get the "original" PageType object from the WrappedPageType. Could you add a Getter method in the WrappedPageType returning the "wrapped" PageType?

    Lars

  7. Joel Abrahamsson's avatar

    Joel Abrahamsson 11 months ago

    Lars,

    Absolutely, I'll make sure to do that.

    Just out of curiosity though, why do you need it?

  8. Lars Kolsaker's avatar

    Lars Kolsaker 11 months ago

    Joel,
    I do not have a deep knowledge of composerbuilder, just did some work to make it work with PTB 2.0.
    The reason we need a PageType object is that the method to register a pagetype in Composer needs an instance of a PageType object :)

  9. Joel Abrahamsson's avatar

    Joel Abrahamsson 11 months ago

    I see. Would it work to do something like var realPageType = PageType.Load(wrapped.ID)?

  10. Lars Kolsaker's avatar

    Lars Kolsaker 11 months ago

    Yes, I think that would solve it

  11. muhammad kashif's avatar

    muhammad kashif 10 months ago

    Hi Joel

    I've a question regarding use of PTB in episerver multilingual site . I'm using PTB 1.3 and EPIServer 6 R2.

    The problem I'm having is that I'm always getting a page (when accessed directly) in fallback language (master language) when page does not exists in its original language . (And it is not set to return the fallback language)

    My initial thoughts are PTB proxy does not return ILanguageSelectionSource interface in PageBase.InitializeCulture() method - and this is where it could be problamatic ?

    Please shed some light is this due to EPIServer /PTB or I'm missing some minor stuff.
    Note : In one of my previous PTB multilingual site we've used the custom friendly url to return a page not found in these scenarios.

    Regards,
    Muhammad

  12. Joel Abrahamsson's avatar

    Joel Abrahamsson 10 months ago

    Muhammad, thanks for reporting this. We'll take a look at it.

  13. Theres some great deals on here! Sometimes companies will extend an expired coupon code by a day or two if you email them. One of my favorite shoe stores, http://www.planetshoes.com/, has some great daily deals and always has different sales during the week.

  14. Bjørn's avatar

    Bjørn 10 months ago

    Any reason why TypedPageData isn't marked [Serializable]?

  15. Joel Abrahamsson's avatar

    Joel Abrahamsson 10 months ago

    Not off the top of my head. What's the use case?

  16. Bjørn's avatar

    Bjørn 10 months ago

    I wanted to save an instance in ViewState. I'm guessing the same issue will arise if attempting to save it in Session. The same is not the case if i use EPiServers baseclass directly, since it is marked as Serializable.

  17. Joel Abrahamsson's avatar

    Joel Abrahamsson 10 months ago

    I would very much advice against storing a PageData object in Viewstate or in sessions. Perhaps you can store the page reference instead? That will be much cleaner and won't send a bunch of data back and forth to the client.

    As for marking the TypedPageData class as serializable it's definitely something worth considering.

  18. Bjørn's avatar

    Bjørn 10 months ago

    Absolutely, I agree, I was just being lazy when trying something out.. Once the issue arose though, I got curious. Glad you'll consider it :-)

  19. Bjørn's avatar

    Bjørn 10 months ago

    I am using PageTypePropertyGroup, and on its properties I specify a DefaultValue. However, this does not always seem to work - although I fail to see a pattern.

    Right now I have a pagetype with two PageTypePropertyGroups on it (let's call them A and B), and what's strange is this: sometimes all the properties in A get their default values set correctly and the properties in B gets EMPTY default values. However, at other times its exactly vice versa - it is the properties in A that get the empty default values while the properties in B get their default values set correctly. It seems random.

    Are you aware of any issues with setting the DefaultValue on properties in PageTypePropertyGroups?

  20. Lee Crowe's avatar

    Lee Crowe 10 months ago

    Hi Bjorn

    I developed the PageTypePropertyGroups functionality and was not aware of any issues. I will put it on my to do list to investigate and fix if I can reproduce it.

    Lee

  21. Erik Nordin's avatar

    Erik Nordin 10 months ago

    I've experienced that without property groups... Don't know why yet.

  22. assurance credit's avatar

    assurance credit 9 months ago

    Great post. I am very happy to know some quality blogs still exist now that have useful information. Thanks for sharing buddy.

  23. Lars Kolsaker's avatar

    Lars Kolsaker 9 months ago

    Hi Joel
    Is the functionality we discussed earlier about retrieving the base Pagetype object implemented yet? You did suggest something like this
    var realPageType = PageType.Load(wrapped.ID)

    lars

  24. Joel Abrahamsson's avatar

    Joel Abrahamsson 9 months ago

    Hey! Actually I think I meant that it should be implemented in ComposerBuilder. Since PageTypeRepository is a class supports PTB which needs the wrapped page type I'm not sure modifying it is the best idea. I might be wrong though as I'm not very familiar with the context.

  25. Lars Kolsaker's avatar

    Lars Kolsaker 9 months ago

    Hi
    The only method I am asking for is to get the PageType object from the WrappedPageType class. A new method in this class, something like this

    public PageType Wrapped
    {
    get { return wrapped; }
    }

    lars

  26. Joel Abrahamsson's avatar

    Joel Abrahamsson 9 months ago

    I hear you, but I'm still not sure why this should be done in PTB. In PTB's domain the WrappedPageType is the page type and while I can't currently see no problem with adding the property I'm not sure it's a good idea do expose an extra public member to solve a specific use case that could easily be solved in other ways. I mean the solution proposed above is almost as easy as getting the wrapped page type from the WrappedPageType instance and since it's cached there's hardly any performance overhead.

    I'm sorry if I'm missing something :)

  27. Lars Kolsaker's avatar

    Lars Kolsaker 9 months ago

    ok, Joel
    I rest my case ;-)

Follow me on Twitter

  1. @tim_abell The gigantic ones are for customers who specifically ask for them only :) 1 months ago
  2. Looking to buy a gigantic easter egg filled with candy for delivery in Stockholm. Any recommendations? 1 months ago
  3. @strandberg_m Du måste skriva om resultatet efteråt! 1 months ago
follow me

Latest comments

  1. Joel Abrahamsson wrote "Hi Jonas! The fluent API is really geared towards working..." on Building a search page for an EPiServer site using Truffler
  2. Jonas wrote "Thank you for one more great write up! If you're not lucky ..." on Building a search page for an EPiServer site using Truffler
  3. David Knipe wrote "The CategoriesFacet method will save me a load of headaches ..." on Cool new features in the Truffler .NET API

About this site

This blog is built with EPiServer Community, EPiServer CMS, ASP.NET MVC and a bunch of other great products. The source code is available for download at the projects page, where you also can read more about this site and my other projects.

read more