Building Dynamicweb Module Admin Interfaces 4 - Using the Ribbon UI - Handling Server Side Events
NOTE: the concepts presented in this article are now considered obsolete possibly because better alternatives are available.
This is part four in a multi-part article series on building custom administrative user interfaces using the Dynamicweb UI controls such as the RibbonBar. If you haven’t read the previous articles, you should check them out first as they contain information that’s used again in this article. You can find the articles here:
- Building Dynamicweb Module Admin Interfaces 1 - Introduction
- Building Dynamicweb Module Admin Interfaces 2 - Using the Ribbon UI – Introduction
- Building Dynamicweb Module Admin Interfaces 3 - Using the Ribbon UI - Handling Client Side Events
As you saw in part three of this series, many of the RibbonBar control have options to trigger behavior at the client. This is nice for pure client side behavior such as switching tabs, or even for server side operations when you use AJAX. However, in many cases you also need the ability to execute server side operations directly. For example, you want to handle the Click event of one of the Save buttons to save the article or handle the Click event of the Copy and Delete buttons to copy or delete the selected articles.
In this article in the series you’ll see how I implemented the following server-side operations in the sample application that comes with this article series (and that you can download at the end of this article):
- Deal with the Copy and Delete buttons to copy and delete selected articles
- Store the Intro Text in the database when you click the Save button on the Settings tab in Default.aspx
- Handle the Save and Save and close buttons in AddEditArticle.aspx. This action also involves working with controls such as the RibbonBarScrollable and RibbonBarCheckbox to read their respective values.
Copying and Deleting articles using the RibbonBarButton
In many ways, dealing with the RibbonBarButton is similar to dealing with standard ASP.NET button controls with one exception. In order for the buttons to trigger server side behavior, you need to set EnableServerClick to true, like this:
<dw:RibbonBarButton Disabled="true" ID="copyButton" Size="Small" Text="Copy" Image="Copy" runat="server" Title="Copy selected articles" EnableServerClick="True" /> <dw:RibbonBarButton Disabled="true" ID="deleteButton" Size="Small" Text="Delete" Image="Delete" runat="server" Title="Delete selected articles" EnableServerClick="True" />
Once you’ve enabled this property on the RibbonBarButton, you need to wire up its Click handler to a method in Code Behind. Because of the lack of design time support for the RibbonBar control, this isn’t as easy as double-clicking the control in Design View. And unfortunately, the current versions of Visual Studio don’t enable you to hook up event handlers in code view. That leaves you to the following options:
- Temporarily move your buttons to outside the RibbonBar; for example, cut them and paste them right before the closing </form> tag. Then switch to Design View and hook up the handler using the Events tab of the Properties Grid for the control. Finally, move the controls back to their original location.
- Manually enter the wiring code in the markup. For a simple button you need an OnClick attribute like this:
<dw:RibbonBarButton Disabled="true" ID="deleteButton" … OnClick="deleteButton_Click" />
and a server side method that accepts an object as the sender argument and a plain EventArgs like this:
protected void deleteButton_Click(object sender, EventArgs e) { }
- Wire up the event handlers in code. In Page_Init or another suitable event, wire up the Click handler like this:
protected void Page_Init(object sender, EventArgs e) { deleteButton.Click += new EventHandler<EventArgs>(deleteButton_Click); }
In recent versions of .NET you can drop the delegate and simple use this:
protected void Page_Init(object sender, EventArgs e) { deleteButton.Click += deleteButton_Click; }
In both cases, as soon as you type += you can press Tab twice to set up the wiring code and create the event handler for you.
You’ll find examples of the two main options (through markup as shown in item 1 and 2) and programmatically (in item 3) in the sample application.
Once the wiring is done, handling the button’s Click event is pretty much standard ASP.NET. In my example, I am using the Entity Framework to display my articles, so I’ll be using EF as well to do the copy and delete operations. The code for copyButton_Click looks like this:
protected void copyButton_Click(object sender, EventArgs e) { string[] articleIds = Request.Form["ArticleId"].Split(new char[] { ',' }); using (CustomModulesSamplesEntities context = new CustomModulesSamplesEntities()) { foreach (string item in articleIds) { int articleId = Convert.ToInt32(item); Article article = context.Articles.Include("Category").Where( a => a.Id == articleId).FirstOrDefault(); if (article != null) { Article newArticle = new Article() { Body = article.Body, Category = article.Category, CreateDateTime = article.CreateDateTime, Deleted = article.Deleted, PublicationDateFrom = article.PublicationDateFrom, PublicationDateTo = article.PublicationDateTo, SeoDescription = article.SeoDescription, Summary = article.Summary, Title = "Copy of " + article.Title, UpdateDateTime = article.UpdateDateTime, CssClass = article.CssClass, Published = false }; context.AddToArticles(newArticle); } } context.SaveChanges(); articleList.DataBind(); } }
This code first gets the selected article ID from the Form collection using Request.Form["ArticleId"] (where ArticleId maps to the name of the checkbox control assigned in part three of this article series). It then splits the value on a comma to get the individual IDs. The remainder of the code then loops through the selected IDs, uses EF to find the associated article, and creates a new article based on the existing one. Each new article is then added to the Articles collection which eventually is saved by calling SaveChanges. The Published property (a new property I added to the model for this article series on UI controls) is set to false so the article isn’t available until the user has modified it and explicitly published it. In the end, I call DataBind on the Repeater control again to force it to display the recent changes to the Articles collection.
Deleting an article is even simpler. It follows the same pattern as you saw with the Copy button, but modifies each article retrieved from EF instead. In my example, deletion is done by setting the Deleted property of an article to true. Clearly, if your requirements differ, it’s just as easy to truly delete the article from the database using context.DeleteObject(article). Here’s the full code for the Delete button.
protected void deleteButton_Click(object sender, EventArgs e) { string[] articleIds = Request.Form["ArticleId"].Split(new char[] { ',' }); using (CustomModulesSamplesEntities context = new CustomModulesSamplesEntities()) { foreach (string item in articleIds) { int articleId = Convert.ToInt32(item); Article article = context.Articles.Where( a => a.Id == articleId).FirstOrDefault(); if (article != null) { article.Deleted = true; } } context.SaveChanges(); articleList.DataBind(); } }
You’ll find some duplication between the Copy and Delete methods, in particular in the way the article IDs are retrieved and parsed. It’s probably better to create a page-scoped property, called SelectedArticleIds for example that does the parsing, and then use that property in both methods. You’ll see how this is done in the next article when copying and deleting articles using the List control is discussed.
You may have noticed that when you press the Copy or Delete button, the Manage tab remains the active one. This is standard behavior where Dynamicweb is able to see which tab caused the post back, and then preselects the correct tab again when the page loads in the browser. It does this by keeping track of the active tab using a hidden field called myRibbon_activeTab. If, for some reason, you want to switch to a different tab after a postback, you can activate the required tab by setting the ActiveTab (which is one-based) like this:
myRibbon.ActiveTab = 1;
With handling multiple selected items done, the next two topics to look at are storing site settings and saving articles. I’ll look at the site settings first as they are handled from Default.aspx as well, and then close off this article by looking at the AddEditArticle.aspx page.
Storing Site Settings
In part 2 of this series I briefly showed you the Settings tab of the RibbonBar (shown again in Figure 1) that enables you store settings for the site.
Figure 1
When you click the Intro Text button, a Dialog control that in turn contains an Editor pops up as shown in Figure 2:
For my example, this is more than enough. In your own application you can add whatever you see fit to the Dialog control. Additionally, you could mix and match controls on the RibbonBar to define “settings” for your application and store them in a database or config file.
As you saw in the article on dealing with clients-side interactions, the Dialog control looks like this:
<dw:Dialog ID="IntroTextDialog" runat="server" Width="700" Title="Intro text" ShowOkButton="true"> <dw:Editor ID="IntroText" runat="server" ForceNew="true" Toolbar="FrontendEdit" Width="564" Height="200" /> </dw:Dialog>
The value of the Editor (or any other control) is available on the server where it can be accessed and stored in the database. For this sample application, I’ve created a simple database table called DvkSiteSettings with a column called IntroText of type nvarchar(max) and an identity column called Id. I then added the table to my Entity Framework model and renamed it by dropping the Dvk prefix so it ended up like this:
Figure 3
With the model done, all I need to do is make sure the Save button can cause a server side postback (by setting EnableServerClick to True) and wiring up the Click handler, like this:
<dw:RibbonBarButton ID="saveButton" Text="Save" Image="Save" runat="server" Title="Save settings" EnableServerClick="True" onclick="saveButton_Click" />
Server side, I handle the Click event as follows:
protected void saveButton_Click(object sender, EventArgs e) { using (CustomModulesSamplesEntities context = new CustomModulesSamplesEntities()) { SiteSetting mySettings = context.SiteSettings.FirstOrDefault(); if (mySettings == null) { mySettings = new SiteSetting(); context.AddToSiteSettings(mySettings); } mySettings.IntroText = IntroText.Text; context.SaveChanges(); } }
This code simply retrieves or creates a single SiteSetting instance, assigns the IntroText property which it retrieves from the Editor control and saves the changes in the database. Because of the built-in behavior of the RibbonBar, the Settings tab remains active. In a later part in this series you see how to use the InfoBar control to communicate back to the user that the save operation has completed.
NOTE: because the Editor submits HTML, you may run into ASP.NET’s Request Validation feature. This is standard ASP.NET behavior that prevents users from submitting malicious code. Since the articles page is located in the protected Admin area, you can be reasonably sure users won’t submit evil code, so you can simply turn off the feature by setting ValidateRequest to False in the @ Page directive:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="Dynamicweb.Samples.Web.CustomModules.DvkArticles.Admin.Default" ValidateRequest="false" %>
To prepopulate the Editor control when the page loads for the first time I use the following code:
protected void Page_Load(object sender, EventArgs e) { if (!Page.IsPostBack) { using (CustomModulesSamplesEntities context = new CustomModulesSamplesEntities()) { SiteSetting mySettings = context.SiteSettings.FirstOrDefault(); if (mySettings != null) { IntroText.Text = mySettings.IntroText; } } } }
Notice how I simply do nothing when mySettings is null. This can happen when you manage the IntroText for the very first time. Saving the IntroText as explained earlier will create the settings object if it doesn’t exist in the database.
With Default.aspx completely Ribbonized, the final page worth looking at is AddEditArticle.aspx that enables you to create and save articles.
Saving Articles
To implement server side functionality in the AddEditArticle.aspx page I added the following behavior:
- Hooked up both Save buttons to a Save handler.
- Hooked up both Save and close buttons to a SaveAndClose handler.
- Both Save and SaveAndClose call a new method called SaveArticle to which they pass a Boolean that determines the final action (reload the page, or redirect to Default.aspx).
- SaveArticle creates a new article or updates an existing one. Most of the code in this article is similar to what you saw in the article series on building custom modules. However, some of it has changed in order to work with the new UI controls.
- I also updated the BindData method to prepopulate some of the controls on the RibbonBar when an existing article is being edited.
You’ve already seen some of the described functionality in earlier sections in this article so I won’t go over the code in full detail. I will however, highlight some of the important pieces. You can find the full source code for the sample application at the end of this article.
I hooked up the Save and Save and close buttons to two methods in code behind like this:
<dw:RibbonBarButton ID="saveButton1" Size="Small" Text="Save" Image="Save" runat="server" EnableServerClick="true" OnClick="SaveButton_Click" /> <dw:RibbonBarButton ID="saveAndCloseButton1" Size="Small" Text="Save and close" Image="SaveAndClose" runat="server" EnableServerClick="true" OnClick="SaveAndCloseButton_Click" /> … <dw:RibbonBarButton ID="saveButton2" Size="Small" Text="Save" Image="Save" runat="server" EnableServerClick="true" OnClick="SaveButton_Click" /> <dw:RibbonBarButton ID="saveAndCloseButton2" Size="Small" Text="Save and close" Image="SaveAndClose" runat="server" EnableServerClick="true" OnClick="SaveAndCloseButton_Click" />
Notice how I hooked up the second set of buttons to the same two methods as the first set of buttons. Although I have defined two sets of buttons on two tabs to make it easy for a user to save an article regardless of the tab they’re on, there’s no need to replicate the behavior they trigger. In Code Behind the methods look like this:
protected void SaveButton_Click(object sender, EventArgs e) { SaveArticle(false); } protected void SaveAndCloseButton_Click(object sender, EventArgs e) { SaveArticle(true); }
Both call SaveArticle and pass a Boolean value that determines whether to redirect or simply display the page again. The complete code for the SaveArticle method looks like this:
private void SaveArticle(bool redirectWhenDone) { Article article; using (CustomModulesSamplesEntities db = new CustomModulesSamplesEntities()) { if (Id > 0) { article = db.Articles.Include("Category").Where( a => a.Id == Id).FirstOrDefault(); if (updateLastModifed.Checked) { article.UpdateDateTime = DateTime.Now; } } else { article = new Article { CreateDateTime = DateTime.Now, UpdateDateTime = DateTime.Now }; db.AddToArticles(article); } article.Body = Body.Text; int categoryId = Convert.ToInt32(CategoryList.SelectedValue); // Line below will be easier with EF for .NET 4 where you can use foreign keys article.Category = db.Categories.Where( c => c.Id == categoryId).FirstOrDefault(); article.CssClass = selectedCssClass.Value; article.PublicationDateFrom = PublishFrom.Date; article.PublicationDateTo = PublishTo.Date; article.SeoDescription = SeoDescription.Text; article.Summary = Summary.Text; article.Title = ArticleTitle.Text; article.Published = published.Checked; db.SaveChanges(); Id = article.Id; if (redirectWhenDone) { EndEditing(); } } }
Most of the code is identical to what you saw previously. There are a few exceptions, though. First, the object initializer for the Article:
article = new Article { CreateDateTime = DateTime.Now, UpdateDateTime = DateTime.Now };
When a new article is created, I assign both the CreateDateTime and the UpdateDateTime today’s date and time. Code in the module’s frontend then only outputs the UpdateDateTime when it differs from CreateDateTime like this:
if (article.CreateDateTime < article.UpdateDateTime) { detailsTemplate.SetTag("UpdateDateTime", article.UpdateDateTime); }
This enables you to display the article’s create date, and optionally the date the article was last updated. Besides the create and update dates (which is merely technical meta data about the article in the database), an Article also has PublicationDateFrom and PublicationDateTo properties. Those are filled based on the selected values in the PublishFrom and PublishTo DateSelector controls:
article.PublicationDateFrom = PublishFrom.Date; article.PublicationDateTo = PublishTo.Date;
Additionally, these dates are taken into account when constructing a LINQ to Entities query to only query articles that are active (where Published is true and Deleted is false) and where today’s date falls within the range specified by the From and To properties. Here’s the LINQ query from the article’s Frontend class that selects all active articles:
category.Articles.Where(c => (c.PublicationDateFrom <= DateTime.Now && c.PublicationDateTo >= DateTime.Now && !c.Deleted && c.Published)).OrderBy(c => c.Title)
At the end of the method, you can see I am assigning the Id of the (new or existing) article to a local property called Id like this:
Id = article.Id;
The property was read-only in earlier articles, but I’ve now added a setter and updated the getter:
private int Id { get { int id; string temp = Request.QueryString["Id"]; if (!string.IsNullOrEmpty(temp) && int.TryParse(temp, out id)) { return id; } object o = ViewState["ArticleId"]; if (o != null) { return Convert.ToInt32(o); } return 0; } set { ViewState["ArticleId"] = value; } }
I need this updated property to handle the case where a user clicks the Save button repeatedly for new articles. Without this code, the Id would return 0 each time it was accessed in SaveArticle because there’s no Query String with an article’s ID when creating a new article. This would create a brand new article each time. By storing the Id returned from the database in View State I can find the existing article that’s currently being added each time the Save button is clicked.
At the end of the method, the user is taken to Default.aspx only when she clicked Save and close by this code:
if (redirectWhenDone) { EndEditing(); }
When just Save is clicked, the page simply refreshes and the user can continue making changes to the article.
The final thing that’s left to do is more related to client side code than it is to server side code, but it only surfaced now that the server side part of the page is done: preselecting the correct item in the RibbonBarScrollable control when an item is being edited. Code in the BindData method reassigns the selected CSS class to the hidden field:
selectedCssClass.Value = article.CssClass ?? string.Empty;
From this point on, the selected value (if present) is available in a hidden HTML field in the page, ready for some Prototype code to use it. To reassign the selected value to the scrollable control, I found it easiest to find the correct item in the list using some Prototype selector syntax, and then simply invoke its click method. This way, I don’t have to worry about finding the row index, the current CSS class and more, and simply delegate this to the code that is already in place. For this behavior, you find the following code in AddEditArticle.aspx:
Event.observe(window, 'load', function () { var selectedClass = $('selectedCssClass').value; if (selectedClass != null && selectedClass != '') { $$('.' + selectedClass).each(function (el) { el.click(); }); } });
This code runs when the DOM is loaded, and then retrieves the selected CSS class from the hidden field selectedCssClass. It then uses the $$ method to find the element (a <div /> in my case based on its CSS class. So, if previously Introduction was selected as the CSS class, the Prototype code looks for all elements with the Introduction class. It then calls its click method which in turn calls handleStyleChange to which it passes the correct style, the row index and a reference to itself, identical to what you saw in the previous article in this series. From this point on, the RibbonBarScrollable is in sync with the currently selected CSS class in the database. Once the user chooses a different class, the hidden field is updated again and its value is stored in the database when the user saves the article.
With the code to sync the RibbonBarScrollable with the selected CSS class from the database, you’ve come to the end of this article that dealt with server side handling of interaction with the RibbonBar and its controls.
In the next, and for the time being last article in this series, you see three final controls used in the sample application: The List, the InfoBar and the ContextMenu control.
Some of the other controls, such as the Tree and the Toolbar control, are not needed in the sample application and aren’t discussed any further for now. If you have a good use case for them and want me to write about them, contact me through my Contact page and I’ll add them to my Todo list.
Downloads
With each article, I'll make a download available with the user interface I've built so far (based on the sample application I built for the article series about developing custom modules). Additionally, you can download the full running demo with all changes up to the latest part in the series. The first download always shows the code from the article you're reading, while the second download is the full example and may contain code that is added or changed in later parts of the series.