Progressive EPiServer Development

Post on 10-May-2015

3,203 views 1 download

Tags:

description

Alternative ways of developing web sites using EPiServer CMS. An introduction to three open source frameworks that allow us to better tackle complexity, have a more enjoyable development experience and deliver better, well tested sites using EPiServer CMS.

transcript

EPiServer Development

I love building beautifull, fast and robust web sites for high profile customers.

When they have editorial content EPiServer CMS provides us with a solid foundation to build upon.

The boring stuff is already in place. We as developers can focus on delivering business value from the get-go.

But, I also have a passion for ”agile” software architecture.

A passion for clean code and flexible systems.

A passion for studying my field.

A passion for learning about principles and practices such as the SOLID principles.

But, I also have a passion for ”agile” software architecture.

A passion for clean code and flexible systems.

A passion for studying my field.

A passion for learning about principles and practices such as the SOLID principles.

While EPiServer is mature after a decade of development it was first released long before many of us had ever heard about Test Driven Development or MVC.

While EPiServer is mature after a decade of development it was first released long before many of us had ever heard about Test Driven Development or MVC.

In other words; it’s built in a way that makes it hard to apply modern development practices and principles.

’ ’

EPiServer development revolves around pages, represented as PageData objects.

While these are primarily intended for editorial content they aren’t limited to that.

In fact we can use them to model just about anything. Orders, events, comments etc.

When we do that we never have to think about how to persist them.

EPiServer has done an excellent job at abstracting the database to the point we’re we take for granted that saving, reading and deleting will just work.

We also instantly get an administrative interface for whatever we have modeled.

And access rights management, support for multiple language versions, versioning and so on.

Of course all of these are optimized for editorial content.

Of course all of these are optimized for editorial content.

But the fact that we can create just about anything without having to think about persistence and get at least some sort of administrative UI is powerful.

Another strength is its flexibility.

I can often find myself frustrated when I try to extend the system, but the fact of the matter is that compared to many other commercial products it’s highly extendable.

And while I would have preferred a more open architecture where I could just switch out or extend standard components there are extension points for pretty much everything.

Often our customers have bought EPiServer for a simple public web site.

But once they’ve bought it, they want us to use it for everything.

In those cases there are three things that frustrate especially much.

Ironically, the thing that frustrate me the most is closely related to the thing that I like the most – the way page types and thereby PageData objects are modeled.

In the database through a UI.

The fact that page types are defined in the database has several negative consequences.

While we’re able to model just about anything in terms of data, we can’t have logic, behavior, in our model objects.

The logic still has to go somewhere.

Often it ends up in the presentation layer.

We’re also not able to use inheritance or polymorphism.

There’s no relationship between different page types.

Similar data has to be modeled multiple times. So does changes.

Deployment is painful.

We can’t just deploy code or scripted database changes. We have to deploy data.

And we have to use magic strings and casts to access data.

Actively bypassing the first test that the compiler otherwise provides us with.

The Dependency Inversion Principle states that our code should “Depend on abstractions, not on concretions”.

This is essential for truly flexible systems.

And for unit testing.

Unfortunately abstractions are scarce in the EPiServer API.

The class we use for saving, retrieving and deleting PageData object, DataFactory, is a singleton with non-virtual methods.

And it doesn’t implement any interface that defines many of its members.

This makes it hard to isolate our code from it, and thereby from the rest of the EPiServer API, the database and HTTP context.

My final, big frustration with EPiServer CMS is its tight coupling to Web Forms.

While I’m not convinced that ASP.NET MVC, or any other MVC framework for that matter, is a perfect fit for many of the sites we build on EPiServer, Web Forms severly limits our ability to create flexible systems built using Test Driven Development.

Even if the EPiServer API would have had abstractions, how would we have gotten concrete implementations of them into our code?

If we had been using ASP.NET MVC we could have create a custom controller factory.

But with Web Forms the framework instantiates components such as user controls.

Not to mention that the page life cycle and on-by-default viewstate functionally isn’t exactly architectural beauty or embracing how the web works.

Unfortunately though, the current version of EPiServer CMS has a few fairly important features such as XForms and Dynamic Content that requires Web Forms.

In 2009 an open source project called Page Type Builder was born.

At its core it does two things…

It scans the application domain for classes that appear to define page types and creates and updates the corresponding page types in the database.

We can create page types using code, and only code. We don’t have to move data when we’re deploying. Only code.

And we can use inheritance between page types.

It also listens to events from the EPiServer API and intercepts requests for PageData objects.

If a PageData object that is about to be returned is of a page type that can be mapped to a class it creates an instance of that class and copies the data from the original object.

Finally, it swaps out the object being returned with the instance of the class.

This means that DataFactory no longer returns plain PageData objects.

It returns instances of classes that we’ve created.

Those still inherit from PageData, meaning that they play nice with the rest of EPiServer’s functionality.

But they can have methods and logic in property getters and setters.

public class Article

{

}

using PageTypeBuilder;

public class Article

{

}

using PageTypeBuilder;

public class Article : TypedPageData

{

}

using PageTypeBuilder;

[PageType]

public class Article : TypedPageData

{

}

using PageTypeBuilder;

[PageType]

public class Article : TypedPageData

{

}

using PageTypeBuilder;

[PageType]

public class Article : TypedPageData

{

}

using PageTypeBuilder;

[PageType(

Filename = "~/Templates/Article.aspx")]

public class Article : TypedPageData

{

}

using PageTypeBuilder;

[PageType(

Filename = "~/Templates/Article.aspx",

Description = "My page type")]

public class Article : TypedPageData

{

}

using PageTypeBuilder;

[PageType(

Filename = "~/Templates/Article.aspx",

Description = "My page type")]

public class Article : TypedPageData

{

}

using PageTypeBuilder;

[PageType]

public class Article : TypedPageData

{

}

using PageTypeBuilder;

[PageType]

public class Article : TypedPageData

{

public virtual string MainBody { get; set; }

}

using PageTypeBuilder;

[PageType]

public class Article : TypedPageData

{

[PageTypeProperty]

public virtual string MainBody { get; set; }

}

using PageTypeBuilder;

[PageType]

public class Article : TypedPageData

{

[PageTypeProperty]

public virtual string MainBody { get; set; }

}

using PageTypeBuilder;

[PageType]

public class Article : TypedPageData

{

[PageTypeProperty(Type = typeof(PropertyString))]

public virtual string MainBody { get; set; }

}

get

{

return this.GetPropertyValue(p => p.MainBody);

}

set

{

this.SetPropertyValue(p => p.MainBody, value);

}

public partial class ArticlePage

: TemplatePage

{

}

using PageTypeBuilder.UI;

public partial class ArticlePage

: TemplatePage<Article>

{

}

using PageTypeBuilder.UI;

public partial class ArticlePage

: TemplatePage<Article>

{

protected override void OnLoad(EventArgs e)

{

var bodyText = CurrentPage.MainBody;

}

}

<html>

<body>

<%= CurrentPage.MainBody %>

</body>

</html>

Our page types can inherit base classes and implement interfaces.

We’re able to use polymorphism.

One page type can return it’s name as link text in menus while another can return something completely different.

While the code that renders the menu only knows that they both implement IMenuItem.

Our page types can have behavior as well as data.

They can have methods.

Properties can have logic in their getters and setters.

We can build page providers that utilize that we’re dealing with different classes.

So far I’ve heard about page providers built using Fluent NHibernate and Raven DB.

We can query using LINQ to Objects in a strongly typed way.

And we can serialize our pages when they are saved and push them over to a search server, to later query for them using a strongly typed, LINQ-like API.

EPiAbstractions is an open source project that wraps EPiServer’s API.

It provides concrete implementations, facades or adapters, that delegate to EPiServer’s classes.

These in turn implement interfaces. Allowing our code to depend on abstractions.

It also provides a simplified API that doesn’t map directly to EPiServer’s but is easier to work with.

For this API it also provides in-memory-implementations, allowing us to write production like code in tests.

Finally it also provides factory classes for creating test data.

[PageType]

public class Article : TypedPageData

{

public IEnumerable<Comment> GetComments(int max)

{

return DataFactory.Instance

.GetChildren(PageLink)

.OfType<Comment>()

.Take(max);

}

}

[PageType]

public class Article : TypedPageData

{

public IEnumerable<Comment> GetComments(int max)

{

return DataFactory.Instance

.GetChildren(PageLink)

.OfType<Comment>()

.Take(max);

}

} ’

[PageType]

public class Article : TypedPageData

{

IDataFactoryFacade dataFactory;

public IEnumerable<Comment> GetComments(int max)

{

return dataFactory

.GetChildren(PageLink)

.OfType<Comment>()

.Take(max);

}

}

protected void Application_Start(...)

{

var container = new Container();

}

protected void Application_Start(...)

{

var container = new Container();

container.Configure(x =>

x.For<IDataFactoryFacade>()

.Singleton()

.Use<DataFactoryFacade>());

}

protected void Application_Start(...)

{

var container = new Container();

container.Configure(x =>

x.For<IDataFactoryFacade>()

.Singleton()

.Use<DataFactoryFacade>());

PageTypeResolver.Instance.Activator

= new StructureMapTypedPageActivator(container);

}

[PageType]

public class Article : TypedPageData

{

IDataFactoryFacade dataFactory;

public Article(IDataFactoryFacade dataFactory)

{

this.dataFactory = dataFactory;

}

public IEnumerable<Comment> GetComments(int max)

{

return dataFactory

.GetChildren(PageLink)

.OfType<Comment>()

.Take(max);

}

}

[PageType]

public class Article : TypedPageData

{

IPageRepository pageRepository;

public Article(IPageRepository pageRepository)

{

this.pageRepository = pageRepository;

}

public IEnumerable<Comment> GetComments(int max)

{

return pageRepository

.GetChildren(PageLink)

.OfType<Comment>()

.Take(max);

}

}

[PageType]

public class Article : TypedPageData

{

IPageRepository pageRepository;

public Article(IPageRepository pageRepository)

{

this.pageRepository = pageRepository;

}

public IEnumerable<Comment> GetComments(int max)

{

return pageRepository

.GetChildren<Comment>(PageLink) .Take(max);

}

}

[Test]

public void GetComments()

{

}

[Test]

public void GetComments()

{

var epiContext = FakeEPiServerContext.Create();

}

[Test]

public void GetComments()

{

var epiContext = FakeEPiServerContext.Create();

var article = CreatePage.OfType<Article>(

epiContext.PageRepository);

}

[Test]

public void GetComments()

{

var epiContext = FakeEPiServerContext.Create();

var article = CreatePage.OfType<Article>();

var articleReference = epiContext.PageRepository

.Publish(article);

}

[Test]

public void GetComments()

{

var epiContext = FakeEPiServerContext.Create();

var article = CreatePage.OfType<Article>();

var articleReference = epiContext.PageRepository

.Publish(article);

article = epiContext.PageRepository

.GetPage<Article>(articleReference);

}

[Test]

public void GetComments()

{

...

var comments = CreateSetOfPageData

.Containing(11)

.PagesOfType<Comment>()

.ChildrenOf(article);

epiContext.PageRepository

.Publish(comments.Cast<PageData>());

}

[Test]

public void GetComments()

{

...

int max = 10;

comments = article.GetComments(max);

}

[Test]

public void GetComments()

{

...

int max = 10;

comments = article.GetComments(max);

Assert.AreEqual(max, comments.Count());

}

In cases where we’re building complex systems, or a particularly complex part of a system, we can address the Web Forms problem by applying the Model View Presenter Pattern.

Model View Presenter, or MVP, is a dialect of the Model View Controller. It has one significant difference compared to MVC; UI events are routed to the view instead of to the controller as in MVC.

While it’s fairly easy to create a basic implementation of MVP with Web Forms there are a number of projects that can help us. One of them is Web Forms MVP.

Given that we’re using Web Forms MVP we can use the open source project EPiMVP which provides some ready-to-use integration with EPiServer.

When using Web Forms MVP and EPiMVP there are four basic components that interact when rendering an ASPX page or a user control; A view (an ASPX or ASCX), a model object (the PageData object), a presenter and a view model.

When a request comes in to a page the Web Forms MVP framework instantiates a presenter and view model object.

The view, which implements an interface which defines members that are relevant for the presenter, is passed to the presenter.

So is the view model object.

The presenter is also aware of the PageData object.

With access to the view, view model and PageData object the presenter populates the view model object with data relevant for rendering in the view.

It can also interact with the view by subscribing to events exposed by it or by calling methods on it.

public class CommentsModel

{

}

public interface ICommentsView : IEPiView<CommentsModel>

{

event EventHandler<SubmitCommentEventArgs> SubmitComment;

}

public class SubmitCommentEventArgs : EventArgs

{

public string AuthorName { get; private set; }

public string Text { get; private set; }

public SubmitCommentEventArgs(

string authorName, string text)

{

AuthorName = authorName;

Text = text;

}

}

public class CommentsPresenter : EPiPresenter<ICommentsView, Article>

{

}

public CommentsPresenter(ICommentsView view,

Article pageData)

: base(view, pageData)

{

}

private IPageRepository pageRepository;

public CommentsPresenter(ICommentsView view,

Article pageData,

IPageRepository pageRepository)

: base(view, pageData)

{

this.pageRepository = pageRepository;

}

protected void Application_Start(...)

{

var container = ...

}

protected void Application_Start(...)

{

var container = ...

PresenterBinder.Factory

= new StructureMapPresenterFactory(container);

} ’

private IPageRepository pageRepository;

public CommentsPresenter(ICommentsView view,

Article pageData,

IPageRepository pageRepository)

: base(view, pageData)

{

this.pageRepository = pageRepository;

view.SubmitComment += SubmitComment;

}

void SubmitComment(object sender, SubmitCommentEventArgs e)

{

}

void SubmitComment(object sender, SubmitCommentEventArgs e)

{

var comment = pageRepository

.GetDefaultPageData<Comment>(

CurrentPage.PageLink);

}

void SubmitComment(object sender, SubmitCommentEventArgs e)

{

var comment = pageRepository

.GetDefaultPageData<Comment>(

CurrentPage.PageLink);

comment.Author = e.AuthorName;

comment.Text = e.Text;

pageRepository.Publish(comment);

}

void SubmitComment(object sender, SubmitCommentEventArgs e)

{

var comment = pageRepository

.GetDefaultPageData<Comment>(

CurrentPage.PageLink);

comment.Author = e.AuthorName;

comment.Text = e.Text;

pageRepository.Publish(comment);

HttpContext.Response.Redirect(CurrentPage.LinkURL);

}

[PresenterBinding(typeof(CommentsPresenter))]

public partial class Comments : EPiMvpUserControl<CommentsModel>,

ICommentsView

{

public event EventHandler<SubmitCommentEventArgs> SubmitComment;

}

<fieldset>

<asp:TextBox ID="CommentAuthor" TextMode="SingleLine" runat="server" />

<asp:TextBox ID="CommentText" TextMode="MultiLine"

runat="server" />

<asp:Button OnClick="SubmitComment_Click"

Text="Post Comment" runat="server" />

</fieldset>

protected void SubmitComment_Click(object sender, EventArgs e)

{

if (SubmitComment == null)

return;

SubmitComment(this,

new SubmitCommentEventArgs(

CommentAuthor.Text,

CommentText.Text));

}

We’ve seen three frameworks that help us address some of the core problems we may face as progressive developers using EPiServer.

With these in our toolbox we’re better equipped to tackle complexity when needed.

Remember to choose the right tools for the job. Be pragmatic.

Page Type Builder can be used in all situations and is likely to have the biggest impact on your overall experience using EPiServer.

EPiAbstractions allow you to create unit tests and to use Test Driven Development.

But it can also be misused. EPiServer CMS and Web Forms wasn’t designed for you to have 100% code coverage.

Don’t even try.

Use it when drive good design of complex systems.

Web Forms MVP and EPiMVP probably seems like it’s complex and hard to understand.

It is.

But it can be a powerful tool to use when you have complex user interactions.

It’s also great for being able to inject dependencies into code that you want to test.

http://pagetypebuilder.codeplex.com

http://epiabstractions.codeplex.com

https://github.com/joelabrahamsson/EPiServer-MVP

http://webformsmvp.com/

http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod

Hand #1: http://www.flickr.com/photos/teacherafael/2243271306/ (with permission)

Hand #2: http://www.flickr.com/photos/fabianluque/5220694009/ (with permission)

Fire: http://www.flickr.com/photos/olemartin/3951562058/

Up/down: http://www.istockphoto.com/stock-photo-4700763-up-down-good-bad-future-past.php

Concrete plant: http://www.istockphoto.com/stock-photo-6416445-young-plant-taking-root-on-a-concrete-footpath.php

Potter: http://www.istockphoto.com/stock-photo-6791142-potter.php

Hands in chains: http://www.istockphoto.com/stock-photo-11441892-dependency.php

Enterprise D: http://www.flickr.com/photos/elzey/523568670/

Chain: http://www.istockphoto.com/stock-photo-2135970-breaking-free.php

Viewstate t-shirt: http://www.flickr.com/photos/piyo/3742166635/

Heart: http://www.flickr.com/photos/saraalfred/3199313309/