I recently had an issue with command timeouts with an EPiServer Community site. The community database is quite large and I had a batch import job that added new entities. During some large maintaince queries on the database that results in locking of the tables, some timeouts occurred in that period. The Community API uses the default timeout which is 30 seconds and there is no buildt in way of changing this with some sort of configuration. To overcome this issue I added the following fix. EPiServer Community 4.0 uses the DatabaseHandler, located in the common framework. I was somewhat surprised to see that this class has both a public instance property as well as public virtual methods. One of these methods is the GetCommand method which have overriden with the following code. public class MyDatabaseHandler : EPiServer.Common.Data.DatabaseHandler
{
public override System.Data.Common.DbCommand GetCommand(bool inTransaction,
string sp,
System.Data.CommandType commandType,
params object[] args)
{
var command = base.GetCommand(inTransaction, sp, commandType, args);
command.CommandTimeout = 1800;
return command;
}
}
Then simply create a EPiServer Community module that instanciates and sets the current DatabaseHandler to our custom db handler.
public class DatabaseModule : IModule
{
public string UniqueName
{
get { return Name; }
}
public string AdministrationControl
{
get { return ""; }
}
public string Name
{
get { return "RelatePlusDatabaseModule"; }
}
public void OnApplicationStart(CommunityContext context)
{
EPiServer.Common.Data.DatabaseHandler.Instance = new MyDatabaseHandler();
}
}
I've never touched EPiServer Composer until last week, and now that I'm used to develop my EPiServer sites using PageTypeBuilder, there was quit a painful transition. Clicky clicky, enter some info, click some more, check if it works, can't drag it in, click some more and so on. And then you must do the same sequence for your test and production environment. To much configuration and no way of synchronizing these settings between the different environments make this prone to errors/human mistakes. I've added a quick little library to solve this and it based on Joel Abrahamsson's PageTypeBuilder framework.
Getting the library
You can find the source code at github or download the binary here. Just add a reference to the ComposerBuilder.dll along with your PageTypeBuilder and EPiServer Composer dlls.
NB The currently project is still in a early stage. You might want to try this in a demo project to ensure nothing bad happens
Creating composer pages
The process of creating episerver composer page is very similar as creating episerver page types using PageTypeBuilder. Instead of creating a typed pagetype that inherits from TypedPageData, you create a ExtensionsPageData object like this.
[ExtensionPageType("1AB03438-55BE-4269-80CD-0C07A0159175",
Name = "Composer Wide Page",
Description = "A wide composer page",
Filename = "~/Composer/Pages/PageWide.aspx")]
public class PageWidePageData : ExtensionPageData
{
[ComposerArea]
public virtual string MainArea { get; set; }
}
Notice the ComposeArea properties. Since Composer by default runs http get request to the pages to discover areas on the page, this method can not be used when using pagetypebuilder since the site is not started yet.
The extension page it self inherits from the ExtensionPage type with the pagetype as a generic type argument like this
public partial class PageWide : ExtensionPage<PageTypes.PageWidePageData>
{
}
In the markup of PageWide.aspx you place you Extension Areas controls where you want the functions to the be placed.
<%@ Page Language="C#" AutoEventWireup="false"
MasterPageFile="~/Templates/Masterpages/Site.master"
CodeBehind="PageWide.aspx.cs" Title="Untitled Page"
Inherits="Demo.Composer.Pages.PageWide" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<div style="min-height:100px;">
<Extension:ExtensionContentArea runat="server"
ID="MainArea" Description="MainArea"/>
</div>
</asp:Content>
The extension page should now be show up as a composer pagetype when creating a new page.
Creating composer functions
Creating the composer functions is a just another pagetype, the process of creating them is done in a similar maner. First you need to create the function type like this
[FunctionType("16126D47-0ABE-4EEB-809D-9795A03A64BA",
Name = "Test", Description = "My Test Function",
Filename = "~/Composer/Functions/TestFunction.ascx",
AvailableOnPages = AvailableOn.Specific,
AvailableOnPageTypes = new [] { typeof(PageTypes.PageWidePageData) },
AvailableOnContent = AvailableOn.All)]
public class TestFunctionData : FunctionData
{
[ComposerProperty(EditCaption = "Text")]
public virtual string Text { get; set; }
}
This class creates a new function pagetype named "Test" and points to corresponding function usercontrol. The functions rules are defined using the AvaiableOnXXX properties. This examples makes this function only available on the page wide pagetype + all content functions that contains composer areas. The function properties is defined using the CompserPropertyAttribute. This attribute creates a corresponding episerver property for the function. In this case an xhtml text.
NB The current release copies the default function security to every function by default.
The next is to create the function itself; the TestFunction.ascx. The codebehind should be modified to inherit from ContentFunction class passing inn the function type.
public partial class TestFunction : ContentFunction<FunctionTypes.TestFunctionData>
{
}
Now simply print out the xhtml using the typed property like this:
<%= CurrentPage.Text %>
To complete the example we can now see the "test" function in the composer toolbox.
Posted on 01 November 2010 and tagged with
EPiServer,
Elmah,
EPiServer,
Logging,
Elmah,
Elmah,
Dynamic Data Store,
Logging,
Logging,
VPP,
Dynamic Data Store,
Dynamic Data Store,
VPP,
VPP
Elmah is a great logging tool to discover unhandled exceptions in you web site. If you haven't used it yet; just add the elmah.dll to your project and couple of settings in your web.config. The log entries is default stored in memory on the server, but you can also choose to store the entries in a permentant store like xml files, a database, twitter etc. I have extended elmah with two new episerver log sinks; Dynamic Data Store and VPP.
[More]
The alternative title to this post could be "stop nagging, thats my wife's job". But enough about that… :-). This is story about a small configuration section that just don't scale that well in larger EPiServer projects. I'm talking about the automaticSiteMapping section inside EPiServerFramework.config file in EPiServer CMS 6. This section contains the mapping between an EPiServer Site and the associated IIS application on the current machine. The configuration looks something like this:
<add key="/LM/W3SVC/17/ROOT:MyComputer" siteId="MySite" />
The issue
The mapping is added automatically to the config file by EPiServer when it does not find an existing valid mapping. The problem with this is that the application needs write access to the EPiServerFramework.config file. Just add write permission to the web folder and your done. Nope, not that simple. For those of you that uses TFS is familiar with the read-only flag on checked in files. This will trigger this wonderful screen at startup, telling you what the file can't be edited.
Now just imagine how this effects a team of 5 developers working on a enterprise site with 25 sites. 5 * 25 = 125 exceptions leading to a check out of the EPiServerFramework.config file, add the mapping and the check in the updated file. Annoying? Yes! Branching of the code branch will even making it worse result in new sites being added with its own host bindings and a new 125 extra mappings (puhh!) to be added for each branch; making this to one big maintenance nightmare.
The workaround (/hack)
One simple solution to get around this is just to do the following
1. Remove all (!!!) mappings inside the automaticSiteMapping section
2. Add the following xcopy statment to post build event on episerver project in Visual Studio
xcopy "$(ProjectDir)EPiServerFramework.config" "$(TargetDir)" /Y
3. Change the path to the EPiServerFramework.config file in web.config like this
<episerver.framework configSource="bin\EPiServerFramework.config" />
These three steps will copy a clean writable instance of the EPiServerFramwork.config file to the bin folder when the solutions is compiled. Resulting in the IIS mapping be added with no problems when the application is started.
One thing i miss in EPiServer is the ability to add custom validation on each page template. You can always listen to the GlobalPageValidation.Validators event, but the PageData returned is not typed and if the validation logic is not divided the end result can turn into one big ugly validation method. This blog entry demonstrates how you can infer custom validation logic using the GlobalPageValidation together with the PageTypeBuilder to validate typed instances of the PageData. Creating the foundations The first thing step is to simply create a base implemetation that inherit from the PageTypeData that contains the basic set with properties needed. In this case the famous "MainBody". The base class also contains a IsValid method that creates a new ValidationResult (which is a simple class that contains a list of errormessages). The method also validates that the name "Smith" is not entered into the MainBody. public abstract class BasePageData : PageTypeBuilder.TypedPageData
{
[PageTypeProperty(Required = true)]
public virtual string MainBody { get; set; }
public virtual ValidationResult IsValid()
{
var result = new ValidationResult();
if (!string.IsNullOrEmpty(MainBody) && MainBody.Contains("Smith"))
{
result.AddError("The MainBody is invalid");
}
return result;
}
}
Your implemations of the TypedPageData will inherit this and override the IsValid method to add their own validation.
Inherit and add more validation
Each inherit instances of BasePageData can override the IsValid method and add their validation error. Here i have added a ActiclePageData type with a Ingress and validation logic to ensure value of the Ingress is at least 100 characters long.
[PageTypeBuilder.PageType("DB0867BB-84BB-432B-A3A5-569DE5B76527",
Name = "[Standard] Acticle page",
Filename = "~/Templates/Test.aspx")]
public class ActicePageData : BasePageData
{
[PageTypeProperty(Required=true)]
public virtual string Ingress { get; set; }
public override ValidationResult IsValid()
{
var result = base.IsValid();
if (string.IsNullOrEmpty(Ingress) || Ingress.Length < 100)
{
result.AddError("You got to write some more in the Ingress");
}
return result;
}
}
Intercept and run the validation
To be able to validate the page data you have to simply subscribe to the GlobalPageValidation.Validators event like this:
EPiServer.GlobalPageValidation.Validators += GlobalPageValidation_Validators;
Then add some code that fetches the PageData from the event, identifies the correct PageData type and runs the validation against using a typed instance
static void GlobalPageValidation_Validators(object sender, EPiServer.PageValidateEventArgs e)
{
// Don't validate if the page is not valid for some other reason
if (!e.IsValid)
{
return;
}
Type type = PageTypeResolver.Instance.GetPageTypeType(e.Page.PageTypeID);
// Current page is not a typed page -> don't validate
if (type == null)
{
return;
}
var activator = new TypedPageActivator();
var typedPageData = activator.CreateAndPopulateTypedInstance(e.Page, type) as BasePageData;
// Current page is not a typed page of type BasePageData -> don't validate
if(typedPageData == null)
{
return;
}
// Validate the type instance
var validationResult = typedPageData.IsValid();
if (!validationResult.IsValid)
{
e.IsValid = false;
e.ErrorMessage = FormatErrorMessages(validationResult.ErrorMessages);
}
If the validation identifies any problems the IsValid property is set to false and a formatted error message is displayed to the web editor like this
This is sequel post of the Using the EPiServer Community API outside IIS where I described how you can call the EPiServer CommunityAPI inside a standalone console application. This post will go a little further and show you how you can import images in the same maner. The issue In my atttempt to create a lot of users with attached my pages profiles i stumbled across a issue when adding portrait images. It turnes out that the ImageGallery module in the EPiServer Community Framework refers directly to the System.Web.Hosting.HostingEnvironment, instead of wrapping it inside a custom object as the EPiServer CMS does it with the EPiServer.Web.Hosting.GenericHostingEnvironment class. The HostingEnvironment is thereby not initialized and calls to e.g. HostingEnvironment.MapPath() will fail. The solution The only way around this is to create a fake web application context. I did not attempt to figure out this on my own, so I hoped somebody had done this before. After some searching i finally found a nice hack onPhil Haack's blog. He have created a HttpSimulator class that is intended to be used when performing unit tests. The HttpSimulator sets private field values using reflection values in the same way i've injected the community configuration. To enable adding images do the follwing: Download the HttpSimulator Unzip and copy the following files into your console application HttpSimulator.cs ReflectionHelper.cs SimulatedHttpRequest.cs Wrapp all of your community api calls inside the following code block using (new Subtext.TestLibrary.HttpSimulator("/", Directory.GetCurrentDirectory()).SimulateRequest())
{
//your community api calls here
}
Example:
The following example creates/gets a test user and creates a new new profile image for that user
//Create web application context + http context
using (new Subtext.TestLibrary.HttpSimulator("/", Directory.GetCurrentDirectory()).SimulateRequest())
{
//*******************************
// Initializing the community api
//*******************************
//Read app config file
System.Configuration.Configuration config = null;
config = System.Configuration.ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
//Inject configuration into the EPiServer Common + EPiServer Community framework
Type type = null;
FieldInfo field = null;
type = typeof(EPiServer.Common.Configuration.EPiServerCommonSection);
field = type.BaseType.GetField("m_config", BindingFlags.Static | BindingFlags.NonPublic);
field.SetValue(null, config);
type = typeof(EPiServer.Community.Configuration.EPiServerCommunitySection);
field = type.BaseType.GetField("m_config", BindingFlags.Static | BindingFlags.NonPublic);
field.SetValue(null, config);
//Initializing the framework
EPiServer.Common.Web.Global.OnBeginRequest();
if (EPiServer.Community.CommunitySystem.CurrentContext == null)
{
throw new Exception("The community context is not initialized");
}
//****************************
// Using the community api
//****************************
//the test user to add a profile image
string username = "TestUser";
//try to find the test user
EPiServer.Common.Security.IUser user = EPiServer.Community.CommunitySystem.CurrentContext.DefaultSecurity.GetUserByUserName(username);
if (user == null)
{
//the test user was not found -> lets create a new user
user = EPiServer.Community.CommunitySystem.CurrentContext.DefaultSecurity.NewUser;
user.UserName = username;
user.Alias = "Java Developer";
user.GivenName = "Test";
user.SurName = "User";
user.PassWord = "SomePassword123";
user.EMail = "testuser@mysite.com";
user.BirthDate = new DateTime(1900, 1, 1);
EPiServer.Common.Data.DatabaseHandler.RunInTransaction(delegate
{
//This will create the user with mypage profile
user = EPiServer.Community.CommunitySystem.CurrentContext.DefaultSecurity.AddUser(user);
});
}
//Get hold of the users mypage profile
EPiServer.Community.MyPage.MyPage profile = EPiServer.Community.MyPage.MyPageHandler.GetMyPage(user);
//make the profile object writable
profile = (EPiServer.Community.MyPage.MyPage)profile.Clone();
if (profile.Portrait == null)
{
//the user profile does not have a image -> lets add a new one
string imagePath = @"Images\JavaDeveloper.jpg";
using (FileStream file = File.Open(imagePath, FileMode.Open))
{
EPiServer.Community.ImageGallery.Image portraitImage
= new EPiServer.Community.ImageGallery.Image("ProfileImage.jpg",
"A random java developer",
file);
portraitImage.PublishState = EPiServer.Common.Publishing.PublishState.Published;
portraitImage.Uploader = profile.User;
profile.Portrait = portraitImage;
EPiServer.Common.Data.DatabaseHandler.RunInTransaction(delegate
{
EPiServer.Community.MyPage.MyPageHandler.UpdateMyPage(profile);
});
}
}
}
And here is the end result:
I have recently come across a scenario where i had to migrate alot of data into EPiServer Community from a legacy system. Since there is quite of a lot of data that needed to be transfered from one system to another, the ideal solutions for us was to do this by code via a console app The console app exports data from the legacy system and inserts it into EPiServer Community by using the powerful community framework. This is however not as simple as it sounds. Here is a little howto on what you have to do to overcome this challenge. 1. Creating a community/relate site Create a new site using the deployment center. This is the community site i want to import data into. In this example i have installed an relate site into the following directory c:\EPiServer\Sites\RelateDemo\Source\Web. Open the project file in the web folder in Visual Studio. Rename the EPiServer.Templates.RelatePlus to Web. Save the solution in by select the solution project in Visual studio and save it via File -> Save as. Store it in the parent folder of the project (c:\EPiServer\Sites\RelateDemo\Source) 2. Creating the console app Create a new console application by selecting the solution file -> new project -> console application. I called my console app MigrationTool. Set the console application as the startup project. The solution should now look similar to this: Now you must add a bounch of references to the EPiServer Common Framework and THe EPiServer Community Framework. Simple right click the console app project (e.g MigrationTool), then select add reference. Add all assemblies found in the following directories: C:\Program Files (x86)\EPiServer\CommonFramework\2.3.517.36\bin C:\Program Files (x86)\EPiServer\Community\3.2.517.24\bin 3. Reusing the community configuration The goal of the console app is to use the community framework in the same matter as the RelatePlus templates does. This requires however a fair amount of configuration in the console apps app.config file and it would basicly look the same as the web.config of the Web project. Instead of creating a new config file for the console app, it would be better to reuse the copying the existing configuration from our web project. Create a script that copies the config files from the web project by right clicking the the console project and select Properites and then select the Build events tab. Add the following script in the Post-Build events textbox xcopy "$(SolutionDir)web\*.config" "$(TargetDir)" /y /q /s copy $(TargetDir)web.config $(TargetPath).config /y Like this Notice the last line of the script. It creates a file called MigrationTool.exe.config so that the console app can read the community configuration. When you build the console app and click the "don't lie to me"-button you will notice the bin folder will now contain the following files: You can now add some code uses the community framework in the console app. E.g fetching the default admin user via the community api: //Using the community api
EPiServer.Common.Security.IUser user = EPiServer.Community.CommunitySystem.CurrentContext.DefaultSecurity.GetUser(1);
Console.WriteLine(user.UserName);
Don't be disapointed. It won't run quite yet.
4. Initializing the Community Framework
If you try to use any of the methods on the community api now you will get a horrible null reference exception since the community framework is not initialize. To work around this you need to trigger the community api to initialize. Add the following code above the code from the previous section:
//Initializing the framework
EPiServer.Common.Web.Global.OnBeginRequest();
5. Mocking the HttpContext
The console app will still not work. I digged far and deep into to the community api and i spoted that the api references a lot of HttpContext.Current. The most notable is the CachingHandler class that relies on the HttpContext.Current.Cache object. To work around this you need to mock the HttpContext. Add a reference to the System.Web.dll in the console app project and add the following code above the code from the previous sections
//Mock HttpContext
string fileName = "default.aspx";
string url = "http://local.relatedemo.com";
string queryString = null;
var sb = new StringBuilder();
var sw = new StringWriter(sb);
var request = new HttpRequest(fileName, url, queryString);
var response = new HttpResponse(sw);
var context = new HttpContext(request, response);
HttpContext.Current = context;
6. The Hack: Injecting the community configuration
Oh no. Another problem. When running the application now it will throw the following exception:
The application relative virtual path '~/' is not allowed here.
It turnes out that the EPiServer Common Framework + EPiServer Community Framework assemblies is reading their configuration by using the the System.Web.Configuration.WebConfigurationManager and providing it with a relative path "~" which won't work outside a web server environment. To avoid the configuration being read using the WebConfigurationMananger you need to inject the configuration into the framework classes yourself.
Add a reference to the System.configuration.dll and add the following code above the previous code.
//Read app config file
System.Configuration.Configuration config = null;
config = System.Configuration.ConfigurationManager.OpenExeConfiguration(typeof(Program).Assembly.Location);
//Inject configuration into the EPiServer Common + EPiServer Community framework
Type type = null;
FieldInfo field = null;
type = typeof(EPiServer.Common.Configuration.EPiServerCommonSection);
field = type.BaseType.GetField("m_config", BindingFlags.Static | BindingFlags.NonPublic);
field.SetValue(null, config);
type = typeof(EPiServer.Community.Configuration.EPiServerCommunitySection);
field = type.BaseType.GetField("m_config", BindingFlags.Static | BindingFlags.NonPublic);
field.SetValue(null, config);
This code reads the configuration from the MigrationTool.exe.config file and injects it into two private static fields deep inside the framework classes using reflection. The EPiServer Common Framework and the EPiServer Common Framework will hence detect the configuration has already been read verifying that the m_config is no longer null and WebConfigurationManager.OpenWebConfiguration("~") is never called :-)
7. The Happy Ending
Finally. The code is working and the alias of the username of the admin is printed to the screen:
Here is the complete source code for this examples:
//Read app config file
System.Configuration.Configuration config = null;
config = System.Configuration.ConfigurationManager.OpenExeConfiguration(typeof(Program).Assembly.Location);
//Inject configuration into the EPiServer Common + EPiServer Community
Type type = null;
FieldInfo field = null;
type = typeof(EPiServer.Common.Configuration.EPiServerCommonSection);
field = type.BaseType.GetField("m_config", BindingFlags.Static | BindingFlags.NonPublic);
field.SetValue(null, config);
type = typeof(EPiServer.Community.Configuration.EPiServerCommunitySection);
field = type.BaseType.GetField("m_config", BindingFlags.Static | BindingFlags.NonPublic);
field.SetValue(null, config);
//Mock HttpContext
string fileName = "default.aspx";
string url = "http://local.relatedemo.com";
string queryString = null;
var sb = new StringBuilder();
var sw = new StringWriter(sb);
var request = new HttpRequest(fileName, url, queryString);
var response = new HttpResponse(sw);
var context = new HttpContext(request, response);
HttpContext.Current = context;
//Initializing the framework
EPiServer.Common.Web.Global.OnBeginRequest();
//Using the community api
EPiServer.Common.Security.IUser user = EPiServer.Community.CommunitySystem.CurrentContext.DefaultSecurity.GetUser(1);
Console.WriteLine(user.UserName);
Now you can create users (with belonging mypage), forum rooms, topics and threads etc. All inside a external non web application.
I hope that this hack is not needed in future releases of the EPiServer Community, but rater add more flexiblity to the framework like using IoC when reading configuration, cach etc so that these small but anoying dependencies can be finally be removed. :-)
The Events Management System is a powerful new feature introduced in EPiServer CMS and is part of the public EPiServer API. It's based on WCF and broadcast messages between servers in the same network using UDP (or TCP). These events is mainly used to send cache update events between the servers. The Events Management System is however not limited to only this usage. Here is a quick how-to on using remote events in your own code. 1. Configure your site to use remote events. 2. Create two new unique guids. The first guid is used to get a handle of the new custom remote event type. The other guid is sent along with the remote event to identify the sender. private static Guid _eventID = new Guid("{CF3321B9-D616-4a35-AA25-A2B7D881D61C}");
private static Guid _raiserID = new Guid("{CF3321B9-D616-4a35-AA25-A2B7D881D61D}");
3. Create a new instance of the remote event client.
private static EPiServer.Events.Clients.Event _remoteEvent;
_remoteEvent = Events.Clients.Event.Get(_eventID);
4. Subscribing to the remote events.
_remoteEvent.Raised += new EPiServer.Events.EventNotificationHandler(RemoteEvent_Received);
///<summary>
/// Receives remote events
///</summary>
///<param name="sender"></param>
/// <param name="e"></param>
static void RemoteEvent_Received(object sender, EPiServer.Events.EventNotificationEventArgs e)
{
//Remote event received -> do some work
System.Diagnostics.Debug.WriteLine("Remote event received: " + e.Param);
}
5. Raising remote events.
/// <summary>
/// Raises a remote event
/// </summary>
/// <param name="param">Information to broadcast to all servers</param>
public static void Raise(string param)
{
if (_remoteEvent != null)
{
_remoteEvent.Raise(_raiserID, param);
}
}
<asp:TextBox ID="txtParam" runat="server" />
<asp:Button ID="btnRaiseRemoteEvent" runat="server" OnClick="btnRaiseRemoteEvent_Click" Text="Raise remote event" />
<script runat="server">
protected void btnRaiseRemoteEvent_Click(object sender, EventArgs e)
{
EPiServer.MyRemoteEventExample.Raise(txtParam.Text);
}
</script>
Thats it. You can download and run the remote event listener to verify that the events are sent correctly across the servers.
Click here to download the complete sample code.
This is a little how-to on making it easier to share your episerver projects between multiple developers by making the location of the episerver resources more abstract.
[More]
EPiServer CMS 5 R2 SP1 is finally here. Happy times :-) The EPiServer developers have really made a quality product. However there are a few features missing in the new Deployment Center tool. One feature I'm missing is the ability to create IIS sites based on the web.config file. This becomes apparent when dealing with larger enterprise solution.
[More]