EPiServer Workflow Replacement : Step 2–GuiPlugin Creation
This is the second in a series of posts about how my company built a replacement workflow platform for EPiServer. Why we chose to do this is explained here: http://world.episerver.com/Blogs/Hans/Dates/2012/4/EPiServer-Workflow-Replacement--Step-1Explanation-and-Disabling-Edit-Tab/
After the base EPiServer Workflow tab is disabled, the next step in integrating our workflow solution is the creation of a new GuiPlugin to replace it. When done, we will have another tab that looks something like this in Edit Mode:
Alright; so to kick things off, we create a standard Control within our code tree. I like to put these in the /Templates/Advanced/Plugins/ folder within my solutions.
In Code Behind, this control should inherit System.Web.UI.UserControl and should be set up as an EPIServer GuiPlugIn like so:
[GuiPlugIn(
Area = PlugInArea.EditPanel,
DisplayName = "Workflow",
Url = "~/Templates/Advanced/Plugins/Workflow.ascx",
RequiredAccess = AccessLevel.Publish)]
GuiPlugins are beyond the scope of this discussion, and there are a lot of great articles out there on these. I recommend Ted Nyberg’s blog at http://labs.episerver.com/en/Blogs/Ted-Nyberg/Dates/2009/2/Adding-a-custom-plugin-button-to-unpublish-a-page-in-EPiServer/ if you’d like to learn more about creating them.
That being said, essentially my above block of code says that this plugin will appear in the EditPanel region of EPiServer within Edit Mode, the Display Name will be “Workflow” (just like the old tab we disabled in Step 1), the URL will be this control we are creating, and users must have Publish access in order to view this tab.
In our Workflow.ascx file on the front end, we begin building out our top area (Quick Version Statistics).
<div style="padding-left:20px; padding-top:20px;">
<h2>Quick Version Statistics:</h2>
<table style="border: 1px solid #000; height: 136px;">
<tr><td width="80px">Version Title: </td><td><%= (Page as EPiServer.PageBase).CurrentPage.PageName %></td></tr>
<tr><td>Version Number: </td><td><%= (Page as EPiServer.PageBase).CurrentPage.WorkPageID %></td></tr>
<tr><td>Version Status: </td><td><%= GetVersionStatus %></td></tr>
<tr><td>Saved By: </td><td><%= UserProfileTools.GetFullName((Page as EPiServer.PageBase).CurrentPage.ChangedBy) %></td></tr>
<tr><td>Page Saved On: </td><td><%= (Page as EPiServer.PageBase).CurrentPage.Saved %></td></tr>
</table>
At this point it is worth noting a few things..first of all, I’m not a front end developer; so you will see some reliance in my markup on things like inline styles and tables. Someone could take this stuff much further and make it look much prettier..but for our purposes, this is enough. Secondly; we have also integrated some other items like our custom User Profile Tools, which essentially just adds first names, last names and pictures to user profiles, as well as our logging tool platform – which basically grabs all actions in the system and logs them in an external table. These are also out of scope for this blog series, so I’d just ignore that stuff if I were you..
I was a big fan of .NET panels when this was originally written, and this application uses them in abundance. Below the Quick version statistics, we add a Rejection Panel to our control, hidden by default, like so:
<br /><br /><br />
<asp:Panel ID="RejectionPanel" runat="server" Visible="False">
<h2>Enter Reason for Rejection:</h2><br />
<asp:TextBox ID="txtRejectReason" runat="server" TextMode="MultiLine" Rows="5" Width="80%"></asp:TextBox><br /><br />
<br />
<EPiServerUI:ToolButton
ID="btnReject"
OnClick="Reject"
DisablePageLeaveCheck="true"
Text="Reject Version"
SkinID="Warning"
ToolTip="Rejects this version of the page"
runat="server" />
</asp:Panel>
You’ll notice this is the first use of EPiServerUI:ToolButton. This needs to be enabled in web.config within the <controls> block around line 378 (mine is just above the EPiServer.WebControls control)
<add tagPrefix="EPiServerUI" namespace="EPiServer.UI.WebControls" assembly="EPiServer.UI" />
Next we add our Standard Panel, which contains most of the Workflow actions that can be performed on this particular version, like so:
<asp:Panel ID="StandardPanel" runat="server">
<h2>Workflow Actions Available:</h2>
<EPiServerUI:ToolButtonContainer ID="ToolButtons" runat="server">
<EPiServerUI:ToolButton
ID="btnDisplayReject"
OnClick="DisplayRejectionPanel"
DisablePageLeaveCheck="true"
Text="Reject Version"
SkinID="Warning"
ToolTip="Rejects this version of the page"
runat="server" />
<EPiServerUI:ToolButton
ID="btnReadyToPublish"
OnClick="ReadyToPublish"
DisablePageLeaveCheck="true"
Text="Ready to Publish"
SkinID="Check"
ToolTip="Mark this version Ready to Publish"
runat="server" />
<EPiServerUI:ToolButton
ID="btnPublish"
OnClick="Publish"
OnClientClick="return confirm('Do you approve of ALL changes on this page?');"
DisablePageLeaveCheck="true"
Text="Publish Version"
SkinID="Publish"
ToolTip="Publishes this version of the page"
runat="server" />
<style type="text/css" media="screen">
span.VersionButton { font-size:11px; vertical-align:top; text-decoration:none; }
</style>
<!--[if lt IE 8]>
<style type="text/css" media="screen">
span.VersionButton { vertical-align:middle; }
</style>
<![endif]-->
<%if (!isPublished) { %><span class="epitoolbutton" Target=""><a href="<%= (GetVersionUrl) %>" target="_blank" style="text-decoration:none; color: #000;"><img src="/App_Themes/Default/Images/Tools/ViewMode.gif" alt="" /><span class="VersionButton">View Version</span></a></span> <% } %>
<span class="epitoolbutton" Target=""><a href="<%= (GetPublishedUrl) %>" target="_blank" style="text-decoration:none; color: #000;"><img src="/App_Themes/Default/Images/Tools/ViewMode.gif" alt="" /><span class="VersionButton">View Published Version</span></a></span>
<br /><br /><br />
<asp:Label ID="lblHistory" runat="server" Text=""></asp:Label>
</EPiServerUI:ToolButtonContainer>
IE8 has a stupid bug that prevents the buttons from displaying properly, hence the hack in the middle of the markup there. This panel is visible by default, and contains buttons for Publishing the page, viewing this version of the page, rejecting the page, and marking the page Ready to Publish.
On to the code behind! First we override OnLoad and grab a writable clone, which we call PageToEdit and make public within the Workflow class. We grab a writeable clone here so EPiServer allows us to edit it (http://sdk.episerver.com/library/cms5/html/M_EPiServer_Core_PageData_CreateWritableClone.htm)
PageData pageToEdit;
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
pageToEdit = (Page as PageBase).CurrentPage.CreateWritableClone();
lblHistory.Text = "<h2>Version History:</h2>" + LoggingTools.GetVersionHistory(pageToEdit);
setButtonVisibility();
RejectionPanel.Visible = false;
StandardPanel.Visible = true;
}
We then add our setButtonVisibility method, which displays buttons based on the status of the page as well as our isPublished and isCheckedIn properties:
protected void setButtonVisibility()
{
btnReject.Visible = !isPublished;
btnPublish.Visible = (!isPublished & isCheckedIn);
btnDisplayReject.Visible = (!isPublished & isCheckedIn);
btnReadyToPublish.Visible = (!isPublished & !isCheckedIn);
}
protected bool isPublished
{
get
{
return pageToEdit.CheckPublishedStatus(PagePublishedStatus.Published);
}
}
protected bool isCheckedIn
{
get
{
if (pageToEdit.Status == VersionStatus.CheckedIn)
{
return true;
}
else
{
return false;
}
}
}
…and then our methods for displaying the proper URL’s for our buttons, GetVersionUrl and GetPublishedUrl, both of which are dependent on an appsettings value called “BaseUrl” which may not be applicable in your environment, but is used in a lot of our application.
protected string GetVersionUrl
{
get
{
String PageVersionUrl = ConfigurationManager.AppSettings["BaseUrl"] + pageToEdit.LinkURL;
PageVersionUrl = PageVersionUrl.Replace("&epslanguage", "_" + pageToEdit.WorkPageID + "&idkeep=True&epslanguage");
return PageVersionUrl;
}
}
protected string GetPublishedUrl
{
get
{
String PagePublishedUrl = ConfigurationManager.AppSettings["BaseUrl"] + pageToEdit.LinkURL;
return PagePublishedUrl;
}
}
…we then implement the GetVersionStatus method, which, admittedly, uses a lot of case statements and could be cleaned up – but basically just looks at the PageStatus and returns some HTML with information based on that.
protected string GetVersionStatus
{
get
{
VersionStatus PageStatus = (Page as PageBase).CurrentPage.Status;
if (PageStatus == VersionStatus.CheckedIn)
{
return "<span style=\"color: #f96;\">Ready to Publish</span>";
}
else if (PageStatus == VersionStatus.CheckedOut)
{
return "<span style=\"color: #f00;\">Not Ready</span>";
}
else if (PageStatus == VersionStatus.DelayedPublish)
{
return "<span style=\"color: #3c3;\">Delayed Publish</span>";
}
else if (PageStatus == VersionStatus.NotCreated)
{
return "<span style=\"color: #0f0;\">Not Created</span>";
}
else if (PageStatus == VersionStatus.PreviouslyPublished)
{
return "<span style=\"color: #06c;\">Previously Published</span>";
}
else if (PageStatus == VersionStatus.Published)
{
return "<span style=\"color: #3c3;\">Published</span>";
}
else if (PageStatus == VersionStatus.Rejected)
{
return "<span style=\"color: #f00;\">Rejected</span>";
}
else
{
return "Unknown Status";
}
}
}
On to rejections! This first method is actually just to display the Rejection panel itself and is pretty self explanatory. The second performs the actual rejection through our PageProviderTools (discussed in post #4) and sends out email notification saying that the item has been rejected.
protected void DisplayRejectionPanel(object sender, EventArgs e)
{
RejectionPanel.Visible = true;
StandardPanel.Visible = false;
}
protected void Reject(object sender, EventArgs e)
{
String RejectionReason = txtRejectReason.Text;
String PageVersionUrl = ConfigurationManager.AppSettings["BaseUrl"] + pageToEdit.LinkURL;
PageVersionUrl = PageVersionUrl.Replace("&epslanguage", "_" + pageToEdit.WorkPageID + "&idkeep=True&epslanguage");
//Get Email Address of Person who submitted page for review
System.Web.Security.MembershipUser SubmittedBy = System.Web.Security.Membership.GetUser(pageToEdit.ChangedBy);
PageProviderTools PPT = new PageProviderTools();
PPT.SetThisPageStatus((Page as PageBase).CurrentPage, VersionStatus.Rejected);
//Send rejection email to person who submitted it
String EmailBody = "<h2>EPiServer System Notification: Content Rejected</h2><table style=\"font-family: Arial; font-size: 12px; \"><tr><td><b>Title:</b></td><td>" + pageToEdit.PageName + "</td></tr><tr><td><b>Rejector:</b></td><td>" + UserProfileTools.GetFullName(pageToEdit.ChangedBy) + "</td></tr><tr><td><b>Version URL:</b></td><td>" + PageVersionUrl + "</td></tr><tr><td><b>Published URL:</b></td><td>" + ConfigurationManager.AppSettings["BaseUrl"] + pageToEdit.LinkURL + "</td></tr><tr><td><b>Description:</b></td><td>This page has been rejected for publishing. Specific details provided below.</td></tr><tr><td><b>Rejection Reason:</b></td><td>" + RejectionReason + "</td></tr></table>";
List<String> EmailList = new List<String>();
EmailList.Add(SubmittedBy.Email);
if (EmailList.Count() > 0)
EmailTools.SendTemplatedEmail("EPiServer Page Rejection - www.mysite.com", EmailBody, EmailList, "EPiServer@episerver.com", "EPiServer");
LoggingTools.LogVersionStatus(pageToEdit, "Rejected", "Rejection Comments: " + RejectionReason.ToString(), PrincipalInfo.Current);
//Reset the panel display
RejectionPanel.Visible = false;
StandardPanel.Visible = true;
//Reset the rejection reason
txtRejectReason.Text = "";
//Reload the current tab ("Workflow")
reload();
}
..then, we add in methods for all of the other buttons in our arsenal.
protected void Publish(object sender, EventArgs e)
{
//Publish the modified page
DataFactory.Instance.Save(pageToEdit, SaveAction.Publish);
//Reload the current tab ("Workflow")
reload();
}
protected void ReadyToPublish(object sender, EventArgs e)
{
//Publish the modified page
DataFactory.Instance.Save(pageToEdit, SaveAction.CheckIn);
//Reload the current tab ("Workflow")
reload();
}
Lastly, we add in FindControl and reload – the latter is used to update the tab with the correct information once it has been submitted, the former is used to find the appropriate TabStrip.
protected void reload()
{
//Find the tab strip
TabStrip actionTabStrip = this.FindControl<TabStrip>(Page, "actionTab");
//Compose a URL for the currently selected tab ("Workflow")
String url = string.Format("EditPanel.aspx?{0}={1}",
actionTabStrip.SelectedTabQueryString,
actionTabStrip.SelectedTab);
//Append ID or it will default to home page on reload
url = url + "&id=" + pageToEdit.PageLink.ID + "_" + pageToEdit.WorkPageID;
lblHistory.Text = "<h2>Version History:</h2>" + LoggingTools.GetVersionHistory(pageToEdit);
//Redirect to the URL to reload the page
Response.Redirect(url);
}
protected T FindControl<T>(Control control, string id) where T : Control
{
T controlTest = control as T;
if (null != controlTest && (null == id || controlTest.ID.Equals(id)))
return controlTest;
foreach (Control c in control.Controls)
{
controlTest = FindControl<T>(c, id);
if (null != controlTest)
return controlTest;
}
return null;
}
In the next post, I’ll describe how all of this comes together from a conceptual level – as this is a lot of code and doesn’t quite put everything into proper context!
Comments