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.
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.
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.
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.
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:
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.
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>();
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>
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.
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.
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
Comments
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 :)
Jarle Friestad 11 months ago
This looks very good :-)
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! :)
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 :)
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. :)
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
Joel Abrahamsson 11 months ago
Lars,
Absolutely, I'll make sure to do that.
Just out of curiosity though, why do you need it?
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 :)
Joel Abrahamsson 11 months ago
I see. Would it work to do something like var realPageType = PageType.Load(wrapped.ID)?
Lars Kolsaker 11 months ago
Yes, I think that would solve it
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
Joel Abrahamsson 10 months ago
Muhammad, thanks for reporting this. We'll take a look at it.
wholesale football jerseys 10 months ago
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.
Bjørn 10 months ago
Any reason why TypedPageData isn't marked [Serializable]?
Joel Abrahamsson 10 months ago
Not off the top of my head. What's the use case?
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.
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.
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 :-)
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?
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
Erik Nordin 10 months ago
I've experienced that without property groups... Don't know why yet.
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.
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
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.
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
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 :)
Lars Kolsaker 9 months ago
ok, Joel
I rest my case ;-)