CurrentPage in Context
For the majority of EPiServer developers, CurrentPage is a fairly straightforward concept. When in a page template or user control, it refers to the current page, and provided that we use the EPiServer Property web control, it intelligently handles our CurrentPage context even in a templated control like a repeater (there is some ‘magic’ inside the Property control that detects if it is in a data bound context and tries to locate a PageData object to use as it’s page data). Even if we don’t use the EPiServer Property web control, the EPiServer templated web controls help us because we can use Container.CurrentPage instead.
It gets a little trickier if we are using a non-EPiServer templated control like a repeater and are not using the EPiServer Property control because we don’t have the EPiServer Property ‘magic’ nor do we have the Container.CurrentPage, so we need to get a little more creative and use a bit of casting like (EPiServer.Core.PageData)Container.DataItem. This is still hardly an issue.
The following table shows what can be used when:
EPiServer Property Control | CurrentPage | Container.CurrentPage | (EPiServer.Core.PageData)Container.DataItem | |
Directly on page/user control | Will detect whether it is inside a data bound control somewhere in the control hierarchy that provides a PageData object, otherwise will use current page | Will always use the current page | Not available | Not available |
In an EPiServer templated control | Will use the page in the data item | Will always use the current page | Will use the page in the data item | Will use the page in the data item |
In a non-EPiServer templated control | Will detect whether it is inside a data bound control somewhere in the control hierarchy that provides a PageData object, otherwise will use current page | Will always use the current page | Not available | Will use the page in the data item |
It is increasingly common to want to create a formatted user control (for example, a news item summary) and use it either singly or in a templated controls such as repeaters across a site. if using only EPiServer Property controls inside the user control this isn’t an issue, because they will detect whether the user control is inside a PageData data item somewhere in the control hierarchy (see table above). However, it is unlikely that this will be useful enough. Nearly always the developer will want to use other controls and inline statements to render property values . The user control itself has no concept of what current page ‘context’ it is in and trying to ‘walk up the tree’ to find a data item context that can be used is tricky – the code inside the EPiServer Property control to do that is fairly complex.
What would therefore be handy is to design our user control in such a way that when it is data bound it ‘knows’ what page context it is in. To do this, I have created a base class that can be used to achieve this. Simply inherit from it and ensure that the ‘PageLink’ property is always set when the control is data bound. Any data-bound controls in your user control will then have access to a CurrentPage object that is always the bound page context. There are versions both for a site with PageTypeBuilder or without (using this base control with PageTypeBuilder sites will also sidestep that nasty not-of-expected-page-type error that you can get when trying to call CurrentPage in a control that inherits from the PageTypeBuilder.UI.UserControlBase<T> class).
Without PageTypeBuilder:
1: public class BoundBase : UserControlBase
2: {
3: public PageReference PageLink { get; set; }
4: private PageData _pageData;
5:
6: public override PageData CurrentPage
7: {
8: get
9: {
10: if (PageLink == null)
11: {
12: throw new Exception("You cannot use BoundBase before the PageLink is set");
13: }
14:
15: if (_pageData == null)
16: {
17: _pageData = GetPage(PageLink);
18: }
19:
20: return _pageData;
21: }
22: set
23: {
24: _pageData = value;
25: }
26: }
27: }
With PageTypeBuilder:
1: public class BoundBase<T> : UserControlBase where T : TypedPageData
2: {
3: public PageReference PageLink { get; set; }
4: private T _pageData;
5:
6: public new T CurrentPage
7: {
8: get
9: {
10: if (PageLink == null)
11: {
12: throw new Exception("You cannot use BoundBase before the PageLink is set");
13: }
14:
15: if (_pageData == null)
16: {
17: var pd = PageTypeBuilder.PageTypeResolver.Instance.ConvertToTyped(GetPage(PageLink));
18:
19: if (pd is T)
20: {
21: _pageData = pd as T;
22: }
23: else
24: {
25: throw new Exception("BoundBase found a page of type " + pd.GetType().Name + " when " + typeof(T).Name + " was expected");
26: }
27: }
28:
29: return _pageData;
30: }
31: set
32: {
33: _pageData = value;
34: }
35: }
36: }
Just a couple of ‘gotchas’ with this:
- Firstly, this technique depends on the PageLink being set. If you are only setting this on data bind (which is most likely), then any attempt to access CurrentPage directly before that in your user control will cause an exception. For example, using <%= CurrentPage.PageName %> would fail. You would need to use <%# CurrentPage.PageName %> instead.
- Secondly, if you are using EPiServer Property controls inside your user control then they will not know about the page context you are using. You’ll need to explicitly tell them by setting their ‘PageLink’ property to data bind to CurrentPage.PageLink, e.g. <EPiServer:Property runat=”server” PropertyName=”PageName” PageLink=’<%# CurrentPage.PageLink %>’/>
I’ve used this technique successfully on various projects, but I’m always open to other ideas so if anyone can think of a simpler way to approach this or has a better technique, please let me know!
I've been using the IPageControl och IPageSource interface and implemented them in a base class for user controls. This way you don't have to specify a PageLink on the user control or the EPi Property controls on it. This is just a snippet of the most relevant code.
private IPageSource _pageSource;
public IPageSource PageSource
{
get
{
if (this._pageSource == null)
{
this._pageSource = this.NamingContainer as IPageSource;
if (this._pageSource == null)
{
this._pageSource = this.Page as IPageSource;
}
if (this._pageSource == null)
{
this._pageSource = DataFactory.Instance;
}
}
return _pageSource;
}
set
{
this._pageSource = value;
}
}
And then;
#region IPageSource Members
public PageData CurrentPage
{
get
{
return this.PageSource.CurrentPage;
}
}
#endregion
If you want to access the "parent" page and not the Container you have to implement PageBase;
public PageBase PageBase
{
get
{
if (this._page == null)
{
this._page = this.Page as PageBase;
if (this._page != null)
{
return this._page;
}
this._page = this.Context.Handler as PageBase;
if (this._page == null)
{
throw new EPiServerException("This user control must be placed on an ASPX-page that inherits from EPiServer.PageBase");
}
}
return this._page;
}
}
And access it by this.PageBase.CurrentPage.
@Johan, that's nice but unless I'm mistaken it only works at one NamingContainer level? In very simple cases it will be OK, but if you have nested user controls or any templated control that isn't looping an IPageSource then you'll need to do a proper hierarchical 'walk' which is rather more complex?
In addition, if you are doing a single item selection then you probably want a PageLink option. Or do you have another way to bind something in a different context to the current page?
That said, I think you're right that implementing IPageSource will mean that my second 'gotcha' will fall away, which will be nice. Question is just how robust I make it from there.
Yep, it will only work at NamingContainer level. But that covers most cases for me.
In IPageSource you have to implement PageLink also.
... And in the IPageSource members you can check if PageLink in IPageControl has a value and then return that page instead of this.PageSource.CurrentPage;