Building Dynamicweb Module Admin Interfaces 5 - Miscellaneous Controls
NOTE: the concepts presented in this article are now considered obsolete possibly because better alternatives are available.
This is part five 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 Event
- Building Dynamicweb Module Admin Interfaces 4 - Using the Ribbon UI - Handling Server Side Events
In this part I’ll show you how to improve the Default.aspx page that shows a list of articles, by using the Dynamicweb List control, together with a ContextMenu control. When finished, the list page will look like figure 1:
Figure 1
Right-clicking an individual item enables you to edit, copy or delete it. The list also enables you to select multiple items using the check boxes in the first column. Additionally, it enables you to select all articles at once using the check box next to the Title column header. The automatic tab switching when one or more articles are selected will still be supported, although it requires a minor change to the existing code. Finally, the list supports sorting (on title and category) in both directions and paging.
Besides the List and ContextMenu controls, I’ll also show you how to use the InfoBar control. Rather than programming that control whenever you need it, I’ll show you a reusable way to embed this control in your pages and how to use Prototype to enhance its appearance (or actually, disappearance as you'll see towards the end of this article).
The List Control
The List control is designed to show items in a list as shown in Figure 1. It’s a very versatile control that enables you to present items in more advanced ways than simply a stack of rows. The control enables you to collapse and expand items (as is the case with products that contain variants in the eCom Product Catalog module for example), sorting, paging and more. The following table lists the control’s most important properties:
Name | Description |
AllowMultiSelect |
When set to true, the control renders checkboxes in front of each row (and next to the first column’s header) to select one or more items in the list. The List control in Figure 1 has multi select turned on. |
ContextMenuID |
The ID of a ContextMenu control to associat with the entire control. In the sample application that comes with this article, I am not using a control based ContextMenu. Instead, each row in the list has its own ContextMenuID set, as you’ll see later in the section “The ContextMenu Control”. Notice that ID is spelled with two capital letters here, unlike the property with the same name on controls such as the RibbonBarButton where it's spelled as ContextMenuId. It's a little inconsistency that you need to live with, especially when you're using a case sensitive language such as C#. |
NoItemsMessage |
Gets or sets the text that is displayed when the control contains no items. This is typically a text such as “No articles found” or “No Products found”. |
Columns |
Enables you to define the columns that the List displays. |
PageSize |
Gets or sets a value that determines the number of items to display on each page of the control when paging is used. |
ShowHeader |
Determines whether or not to show the column headers (the Title and Category headers in Figure 1). |
ShowPaging |
Determines whether or not to show the paging bar at the bottom of the control. |
ShowTitle |
Determines whether or not to show the title that appears above the control (the row with the text Articles in it in Figure 1) |
Title |
Gets or sets the title that appears above the control (the row with the text Articles in it in Figure 1) |
OnClientCollapse |
Defines the JavaScript to run when an item is collapsed. |
OnClientUnCollapse |
Defines the JavaScript to run when an item is expanded. |
OnClientSelect |
Defines the JavaScript to run when you change the selection at the client. Despite what its name may suggest, it runs the JavaScript when an item is selected and deselected, and also runs it when you use the Check All or Uncheck all checkboxes. As such, it’s an ideal property to replace the custom Prototype code that used Event.observe to hook up the check boxes to the client selectedArticlesChanged function that you saw in the preceding article in this series. |
Besides the properties, the control features a number of useful server side events, such as:
Name | Description |
PageIndexChanged |
Fires when a user navigates from one page to another. You use the event to rebind the list to your data source. |
Sort |
Fires when a user sorts the data in the list. The event handler receives an ListSortEventArgs argument that enables you to determine the sort direction and the column to sort on. You see how this works later. |
Finally, the control has a number of methods, of which the most interesting are:
Name | Description |
Clear |
Removes all rows from the control. |
AddRow |
Adds a new row (of type ListRow) to the control. |
GetSelectedRows |
Returns a ListRowCollection of rows that are currently selected. The control doesn’t maintain the rows in View State so if you access this method after a postback, you need to bind it to your data first. |
RemoveRow / RemoveRowAt |
Enables you to remove a row based on either a reference to a ListRow or on its index. |
There are more features the List control supports that I am not including here, such as filtering. The sample application doesn’t require it so I won’t discuss these features for now.
To implement the List control in my Default.aspx page, I went through the following changes. First, I deleted the Repeater that displayed the articles and replaced it with the following List control:
<dw:List ID="articleList" Title="Articles" PageSize="20" runat="server" OnPageIndexChanged="articleList_PageIndexChanged" NoItemsMessage="No articles found" OnSort="articleList_Sort" AllowMultiSelect="true" OnClientSelect="selectedArticlesChanged();"> <Columns> <dw:ListColumn EnableSorting="true" Name="Title" Width="550" runat="server" /> <dw:ListColumn EnableSorting="true" Name="Category" Width="200" runat="server" /> </Columns> </dw:List>
Next, I created a private method called LoadArticles (which is called from Page_Load and various other methods) that contains the following code:
private void LoadArticles() { using (CustomModulesSamplesEntities context = new CustomModulesSamplesEntities()) { articleList.Clear(); var allArticles = context.Articles.Include("Category").Where( a => !a.Deleted).OrderBy(SortColumn + " " + SortDirection.ToString()); foreach (var article in allArticles) { ListRow row = new ListRow(); row.AddColumn(article.Title); row.AddColumn(article.Category.Description); row.OnClientClick = string.Format("editCurrent({0});", article.Id); row.ContextMenuID = "articleMenu"; row.ItemID = "Item" + article.Id.ToString(); row.RowID = article.Id; articleList.AddRow(row); } } }
First, the code gets all articles that are not deleted. It then sorts them using an extension method from the LINQ Dynamic Query Library (which you’ll see more about in a minute). If then loops over the articles and creates a new ListRow item, the child controls that are displayed in the List control. The code then assigns the article’s Title and the Category’s Description to be displayed in the list. The OnClientClick property is assigned a reference to a client side editCurrent function which gets passed the ID of the article. The ItemID ends up as a client side id of the HTML. That’s why I am prefixing it with the text “Item” to make it a valid identifier in HTML (numeric id attributes are not allowed and cause all kinds of problems). The RowID gets assigned the ID of the article and is used later to find out which rows to edit, delete or copy. At the end, the new row is added to the articleList which eventually displays them on the page.
Since I am using the Entity Framework, dynamically sorting data can be tricky. One solution to the problem is to manually code different sorting scenarios, such as :
if (SortColumn == "Title") { results = allArticles.OrderBy(a => a.Title); } if (SortColumn == "Category.Description") { results = allArticles.OrderBy(a => a.Category.Description); }
However, this gets ugly quite quickly, especially when you have many different columns to sort on. Another alternative is to use Entity SQL, but that may be a bit hard to get used to. Finally, you can use the LINQ Dynamic Query Library) that “the Gu” has blogged about a long time ago: http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx. Using that library (you find its complete source in the sample project) you can do string based ordering and filtering. That means you can now do stuff like this:
var allArticles = context.Articles.Include("Category").Where( a => !a.Deleted).OrderBy(SortColumn + " " + SortDirection.ToString());
where SortColumn contains a string with a column name (such as Title to sort on an article’s Title, or Category.Description to sort on the Description property of an article’s category.)
The two properties, SortColumn and SortDirection are View State properties and are defined as follows:
public string SortColumn { get { object o = ViewState["SortColumn"]; if (o != null) { return o as string; } return "Title"; } set { if (value.ToLower() == "category") { value = "Category.Description"; } ViewState["SortColumn"] = value; } } public ListSortDirection SortDirection { get { object o = ViewState["ListSortDirection"]; if (o != null) { return (ListSortDirection)o; } return ListSortDirection.Ascending; } set { ViewState["ListSortDirection"] = value; } }
These properties follow the standard View State properties pattern to ensure their value is maintained across postbacks. There’s one catch for the SortColumn property. The Dynamicweb ListColumn control doesn’t allow you to specify a SortExpression. Instead, it used the Name. For an end user, a header text such as Description or Category would make the most sense. But in order to dynamically sort on the description of the category, you need a string like Category.Description. That’s the reason for this code in the property’s setter:
if (value.ToLower() == "category") { value = "Category.Description"; }
When you want to sort on the category, the value Category.Description is stored in View State. It’s this value that’s used in LoadArticles to determine the sort column.
The SortDirection and SortColumn properties get a value in the Sort event handler:
protected void articleList_Sort(object sender, ListSortEventArgs e) { SortDirection = e.Direction; SortColumn = e.Column.Name; LoadArticles(); }
The e argument exposes the Direction and Column.Name properties which are then stored in the View State properties. Next, LoadArticles is called to rebind the data (with the new sort column and order applied). LoadArticles is also called when the user moves from one page to another:
protected void articleList_PageIndexChanged(object sender, EventArgs e) { LoadArticles(); }
With this code in place, the list now displays all articles from the system and enables you to sort them on different columns, and in both directions.
Because the selected IDs that determine which article to copy and delete no longer come from my own custom checkboxes in the Repeater (as you saw previously), I needed to modify the Copy and Delete methods. Since they contained some duplication, this was a nice occasion to refactor some of the code to some helper property. In the Code Behind of Default.aspx, you find the following property:
private List<int> SelectedArticleIds { get { List<int> tempResult = new List<int>(); // other code here; shown later LoadArticles(); foreach (ListRow item in articleList.GetSelectedRows()) { tempResult.Add(item.RowID); } return tempResult; } }
This code rebinds the List control by calling LoadArticles. It then calls GetSelectedRows to get all the rows the user selected, adds the RowID of that row to a generic list of integers and returns that. Calling code can now be simplified to this:
foreach (int articleId in SelectedArticleIds) { // Work with the articleId here }
The next step is to change the selectedArticlesChanged function that is called when a user changes the selected items in the list. Previously, the Prototype code targeted the table that contained the check boxes. Now, with the List control, you can target the ID ListTable, which the List assigns to the table containing your items. The only change to the function is in the selector syntax:
var numberOfSelectedArticles = $$('#ListTable input[type=checkbox]:checked').length;
This now finds the correct check boxes in the list, and then uses them to determine the active tab, and whether or not the Delete and Copy buttons are active, just as you saw in part three of this series.
Since the OnClientSelect property of the List control now ensures that selectedArticlesChanged is called whenever a change is made in the list, I could also remove all code that started with Event.observe(window, 'load', function () and that hooked up the check boxes to the client function.
When adding rows to the List control, you may have noticed how I set up the OnClientClick:
row.OnClientClick = string.Format("editCurrent({0});", article.Id);
With this code, the editCurrent function is called when you click an item. The editCurrent function is defined in the page and looks like this:
function editCurrent(articleId) { location.href = 'AddEditArticle.aspx?Id=' + articleId; }
It simply accepts the ID of the article and then redirects the user to AddEditArticle.aspx and forwards the ID. I could have in-lined the redirect when assigning OnClientClick a value, but I intend to reuse the editCurrent function later from a context menu which you see more about later in this article.
With this code, users now have the following options:
- They can click an item in the list to go to AddEditArticle.aspx and edit the article.
- They can select one or more articles and click the Copy button to create copies of the selected articles.
- They can select one or more articles and click the Delete button to delete the selected articles.
However, it’s not uncommon to give users more options: through a context menu that appears when you right click a row in the list. You see how this works in the next section.
The ContextMenu Control
When you right-click an article in the list with articles in the sample application that comes with this article series, you get a context menu, as shown in Figure 2:
Figure 2
The three different menu items would execute the same operations mentioned earlier, but Copy and Delete now operate on the clicked item only. The ContextMenu control is pretty easy to use, yet quite powerful. To define one in your page, you need code like this:
<dw:ContextMenu ID="articleMenu" runat="server"> <dw:ContextMenuButton ID="ContextMenuButton3" Text="Edit" Image="EditDocument" runat="server" OnClientClick="editCurrent(ContextMenu.callingID);" /> <dw:ContextMenuButton ID="ContextMenuButton1" Text="Copy" Image="Copy" runat="server" OnClientClick="copyCurrent(ContextMenu.callingID);" /> <dw:ContextMenuButton ID="ContextMenuButton2" Text="Delete" Image="Delete" runat="server" OnClientClick="deleteCurrent(ContextMenu.callingID);" /> </dw:ContextMenu>
For each menu item you want to display you define a ContextMenuButton which looks a bit like other Dynamicweb buttons. You assign a Text, an Image or ImagePath property (the latter is used for custom images) and an OnClientClick handler which is called when the menu item is chosen. Each item in the List control is hooked up to this ContextMenu control using the code you saw previously:
row.ContextMenuID = "articleMenu";
Notice how each of the menu items calls a client-side function: editCurrent, copyCurrent, and deleteCurrent respectively. Each of these methods gets passed the ID of the row that’s being clicked through ContextMenu.callingID. This is a static member on the client-side ContextMenu class which always returns the RowID associated with the item being clicked. So, for example, when you right-click article 23 and choose Edit, editCurrent is called with the number 23. The same is true for copyCurrent and deleteCurrent. However, these methods first store the selected ID in an asp:HiddenField and then call the associated button’s click method on the RibbonBar. For example, copyCurrent looks like this:
function copyCurrent(articleId) { $('SingleArticleId').value = articleId; Ribbon.enableButton('copyButton'); $('copyButton').click(); }
Note that’s important to enable the button first, or it won’t cause a post back.
At the server, you can retrieve the selected ID using SingleArticleId.Value. In order to reuse the server side Copy and Delete methods so they can delete single items as well, I updated the SelectedArticleIds property (which you saw earlier), to return a single ID (still wrapped in a list so you can iterate over it), when this hidden field contains a value. SelectedArticleIds now looks like this:
private List<int> SelectedArticleIds { get { List<int> tempResult = new List<int>(); string tempValue = SingleArticleId.Value; if (!string.IsNullOrEmpty(tempValue)) { SingleArticleId.Value = string.Empty; tempResult.Add(Convert.ToInt32(tempValue)); return tempResult; } LoadArticles(); foreach (ListRow item in articleList.GetSelectedRows()) { tempResult.Add(Convert.ToInt32(item.RowID)); } return tempResult; } }
When SingleArticleId contains a value, I add it to the list, clear the hidden field (so the value doesn’t stick around) and then return the list. If the hidden field does not contain a value, the code proceeds as previously by retrieving the selected IDs from the List control using GetSelectedRows.
Now that you’ve seen how to use the ContextMenu control to trigger client side code, the final change to the sample application I am going to show is a way to inform the user something happened by using the InfoBar control.
InfoBar
As you saw in the first article in this series, the InfoBar displays a message to the user. You can determine the type of message (Information, Error, and Warning) and optionally supply a custom icon. Here’s a quick reminder of the various types:
Figure 3
One way to use this control is to add it to a page and hide it. Then whenever you need to display it, you assign a Message and a Type, and make the control visible. However, there’s a better way to use this control that enables you centralize its code, providing a chance to define the control’s look and behavior from a single location. Here’s how I accomplished that in the sample application.
First, I added a standard ASP.NET User Control called InfoBar.ascx to the project and added the following markup:
<div id="InfoBarWrapper" runat="server"> <dw:Infobar ID="infobar1" runat="server" /> <script type="text/javascript"> Effect.SlideUp.delay(2, 'InfoBar_infobar1', { duration: 2.0 }); </script> </div>
Besides the InfoBar control you also see some Prototype code that uses the delay function to automatically slide up (and thus hide) the info bar after 2 seconds.
Next, I created a custom InfoBarSettings class with the following code:
public class InfoBarSettings { public static InfoBarSettings Current { get { return HttpContext.Current.Session["InfoBarSettings"] as InfoBarSettings; } } public static void Remove() { HttpContext.Current.Session["InfoBarSettings"] = null; } public static void CreateMessage(string message, Infobar.MessageType type) { InfoBarSettings settings = new InfoBarSettings() { Message = message, Type = type }; HttpContext.Current.Session["InfoBarSettings"] = settings; } public string Message { get; set; } public Infobar.MessageType Type { get; set; } }
The class exposes the following members:
Name | Description |
Message |
The message to display on the InfoBar control. |
Type |
The type of message (an Infobar.MessageType) used to determine the type of message to display. |
Current |
Returns an instance of InfoBarSettings from session state when present. Returns null otherwise. |
CreateMessage |
Creates a new instance of the InfoBarSettings class and stores it in the user’s session. This is a static method which means client code can simply call CreateMessage on this class to create a new InfoBarSettings instance in session state. |
Remove |
Removes the current InfoBarSettings instance from session state. This method is called by the User Control when it’s done displaying the current InfoBar. |
With this class in place, I could now add the following code to the User Control’s Code Behind:
public partial class InfoBar : System.Web.UI.UserControl { protected void Page_PreRender(object sender, EventArgs e) { InfoBarSettings info = InfoBarSettings.Current; if (info != null) { // Clear the value from session InfoBarSettings.Remove(); infobar1.Message = info.Message; infobar1.Type = info.Type; InfoBarWrapper.Visible = true; } else { InfoBarWrapper.Visible = false; } } }
This code first checks if there is a current InfoBarSettings in session state that needs to be displayed by accessing the Current property. If there’s no current instance, the control hides all of its children by setting the Visible property of the wrapper to false. But if there is a current instance, it uses that to set up the Dynamicweb InfoBar and deletes the current instance from memory. This way, the next time the page loads, there’s no current instance and the InfoBar is no longer visible.
Since PreRender runs pretty late in the page's life cycle, other pages have enough time to create a new message. In the sample application you find a definition of the user control like this in AddEditArticle.aspx:
<uc1:InfoBar ID="InfoBar1" runat="server" />
(You find a similar control in Default.aspx that displays the list). In the SaveArticle method, you find this code:
InfoBarSettings.CreateMessage("Article saved successfully", Infobar.MessageType.Information);
What I like about this setup, is that the SaveArticle method isn’t bothered with determining where or how to display the message; it just signals that a message needs to be displayed. If the user clicks Save, AddEditArticle.aspx simply reloads, saves the article and then the User Control InfoBar.ascx sees that a message needs to be displayed on that page. If, however, you click Save and close, the user is redirected to Default.aspx. There, the same routine fires: the Infobar.ascx control sees that a message is requested (because the item returned from the Current property is not null) and displays it. In both cases, the InfoBarSettings is removed from the session, so it’s no longer displayed on the next request (for example, when paging or sorting in the list with articles).
If you take a look at the code for Default.aspx.cs, you find similar code that creates messages for the copy, delete and save actions. So, directly after you’ve copied one or more articles, you see something like this:
Shortly after that, the message begins sliding up:
and eventually completely disappears:
To make this solution even more reusable, you can use ASP.NET Master Pages and define the InfoBar User Control there. Then, regardless of the page you’re on, you can display a custom message with a single line of code.
With the InfoBar control, I’ve come to the end of this article series on building custom user interfaces with the Dynamicweb UI controls. I haven’t discussed each available control, and I haven’t discussed each and every member or behavior, but hopefully I’ve given you enough insight in how they work so you can apply that knowledge to other controls.
I can wholeheartedly recommend using the Dynamicweb controls in your admin pages. Most of them are relatively easy to use so you can start using them in no-time. Your users will appreciate your efforts as they’ll find it easier to work with your modules as they look and behave similar to the built-in Dynamicweb modules.
Feel free to contact me through my Contact page if you have follow up questions about these articles, or if you have requests for an article, whether that’s about the Dynamicweb controls, or other custom functionality.
Happy coding!
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.