HXH
HXH
Apr 28, 2012
  5493
(0 votes)

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:

image

 

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!

Apr 28, 2012

Comments

Please login to comment.
Latest blogs
Opti ID overview

Opti ID allows you to log in once and switch between Optimizely products using Okta, Entra ID, or a local account. You can also manage all your use...

K Khan | Jul 26, 2024

Getting Started with Optimizely SaaS using Next.js Starter App - Extend a component - Part 3

This is the final part of our Optimizely SaaS CMS proof-of-concept (POC) blog series. In this post, we'll dive into extending a component within th...

Raghavendra Murthy | Jul 23, 2024 | Syndicated blog

Optimizely Graph – Faceting with Geta Categories

Overview As Optimizely Graph (and Content Cloud SaaS) makes its global debut, it is known that there are going to be some bugs and quirks. One of t...

Eric Markson | Jul 22, 2024 | Syndicated blog

Integration Bynder (DAM) with Optimizely

Bynder is a comprehensive digital asset management (DAM) platform that enables businesses to efficiently manage, store, organize, and share their...

Sanjay Kumar | Jul 22, 2024

Frontend Hosting for SaaS CMS Solutions

Introduction Now that CMS SaaS Core has gone into general availability, it is a good time to start discussing where to host the head. SaaS Core is...

Minesh Shah (Netcel) | Jul 20, 2024

Optimizely London Dev Meetup 11th July 2024

On 11th July 2024 in London Niteco and Netcel along with Optimizely ran the London Developer meetup. There was an great agenda of talks that we put...

Scott Reed | Jul 19, 2024