Content lister directories in Umbraco

This post is extending the one about the angular property editor. I’ve developed the property editor and functionality to create a content lister that shows child pages on the front end using MVC webgrids and allows the user to configure which properties are sortable, filterable etc.

This is very much a work in progress but the basic idea is that a user could create a load of child pages below a landing page. The landing page then contains a control that lists these children, allowing sorting filtering, searching etc. When one of the children are selected, a modal pops up showing the content. The landing page contains configuration that allows the lister to set things like number of results per page, whether to include a search, and also allows a document type to be selected. This in turn allows properties of that document type to be selected to be used as column headings.

directory-configThe directory listing options was the tricky bit to begin with. So, once a document type is selected, each property can be configured. At the moment the output looks something like this (which is less than impressive I realise)…

directory-outputThe property editor

Most of the work for this is explained in the post I mentioned before. A few additional parameters were added to get the other config options (sortable, filterable, searchable etc). The function to get the properties for the selected document type from the .net api now looks like this…

public object GetPropertiesForDocType(int id)
{
var props = new List<object>();
foreach (var prop in Services.ContentTypeService.GetContentType(id).PropertyTypes){
props.Add(new {  
 id = prop.Id,  
 alias = prop.Alias,  
 selected = false, 
 displayName = prop.Name,
 filterable = false,
 sortable = false,
 searchable = false,
 linkToDetails = false,
 propertyEditorAlias = Services.DataTypeService.GetDataTypeDefinitionById(prop.DataTypeDefinitionId).PropertyEditorAlias});
}
return props;
}

Grabbing the actual property editor alias is important because it allows properties to be output in the correct way. Obviously, when the lister renders on the page, it has no way of knowing what child pages it will be rendering, so each property has to be output based on its type.

Working backwards

I kind of knew what I wanted to accomplish, but it took a few revisions to get there. For example, I started off trying to use the rather snazzy gridmvc, which has some very nice features. However, for reasons that will hopefully become clear, this was not usable in the end.

The Grid

Here’s the code that outputs the grid…

@if (Model.DirectoryItems.Any())
{
@*---- 1 ----*@
var grid = new WebGrid(
 rowsPerPage: Model.DirectoryItemsPerPage,
 selectionFieldName: "selectedRow",
 sortDirectionFieldName: "sortDir",
 sortFieldName: "sortField",
 ajaxUpdateContainerId: "directory-container");

@*---- 2 ----*@
WebGridColumn[] columnSet = (
 from dh in Model.DirectoryHeaders
 select new WebGridColumn()
 {
  ColumnName = dh.Alias,
  Header = dh.DisplayName,
  Format = (item) => FormatDirectoryElement(item, dh),
  CanSort = dh.Sortable,
 }).ToArray<WebGridColumn>();

@*---- 3 ----*@
grid.Bind(Model.DirectoryItems, Model.DirectoryColumnNames, autoSortAndPage: false, rowCount: Model.DirectoryItemCount);

@*---- 4 ----*@
<div id="directory-container">
@grid.GetHtml(
 columns: columnSet,
 tableStyle: "responsive",
 headerStyle: "directoryHeader",
 selectedRowStyle: "selectedDirectoryRow",
 mode: WebGridPagerModes.All)

@*---- 5 ----*@
@if (grid.HasSelection)
{
 Dictionary<string, DirectoryElement> directoryElements = grid.Rows[grid.SelectedIndex].Value;
 <ul class="no-bullet">
  @foreach (KeyValuePair<string, DirectoryElement> kvp in directoryElements)
  {
   <li>@Html.Raw(MapElement(kvp.Value))</li>
  }
 </ul>
 }
</div>
}
  1. The grid is instantiated using the DirectoryItemsPerPage from the model, and a bunch of other stuff, mainly to set css classes on the grid
  2. Since it is unknown how many and what kind of columns are going to be used, the columns collection is created looping through the DirectoryHeaders collection which is also part of the model. This collection mirrors the collection passed from the property editor but creates strongly typed ‘DirectoryHeader’ objects rather than just JObject’s. The values set are the actual column name, the name that is displayed, the method that defines how grid elements are output (see Formatting  elements) and whether the column is sortable (which was set in the property editor)
  3. The Bind method of the grid accepts the actual data, a collection of column names to use and other options which in this case are used to override the default (crap) paging method of mvc webgrid (it has to load the entire data set in then calculate the page to show and pager options).
  4. The GetHtml method actual renders the grid. It accepts the column collection and various config options to set css classes for stuff like headers, footers, and set paging options
  5. If a selection was made, the row is accessible via the grid.SelectedIndex property. The actual row can then be output.

Formatting elements

Without some kind of method to format the data being output in the grid, it would all just be output as text. That’s fine for text input but what about media pickers, date times, true/false? So, a helper method ‘FormatDirectoryElement’ exists to check the propertyeditoralias and output the value accordingly. Here it is…

@functions{
string MapElement(DirectoryElement element)
{
 string output;
 switch (element.DirectoryElementType)
 {
  case "Umbraco.TrueFalse":
   output = string.Format("<span>{0}</span>", bool.Parse(element.DirectoryValue) ? "Yes" : "No");
  break;
  case "Umbraco.MediaPicker":
   IPublishedContent media = string.IsNullOrEmpty(element.DirectoryValue) ? null : Umbraco.TypedMedia(int.Parse(element.DirectoryValue));
   output = media != null ? string.Format("<img src={0} alt={1} />", media.GetCropUrl("thumbnail"), media.GetPropertyValue<string>("altText")) : "no image available";
   break;
   case "Umbraco.Date":
    output = string.Format("<span>{0:dd MMM yyyy}</span>", DateTime.Parse(element.DirectoryValue));
   break;
   default:
   output = string.IsNullOrEmpty(element.DirectoryValue) ? "<span>unspecified</span>" : string.Format("<span>{0}</span>", element.DirectoryValue, element.DirectoryElementType);
   break;
   }
   return output;
}
}

@helper FormatDirectoryElement(WebGridRow row, DirectoryHeader header)
{
Dictionary<string, DirectoryElement> item = row.Value as Dictionary<string, DirectoryElement>;
DirectoryElement element = item[header.Alias];
string output;
if (element != null)
{
 output = MapElement(element);
 if (element.IsLinkToDetails)
 { 
  output = row.GetSelectLink(element.DirectoryValue).ToString();
 }
 @Html.Raw(output)
}
}

The function and helper get the current row and according to its type, output the appropriate markup. Obviously there are properties that aren’t covered here but at the moment a default just renders the value as text.

The controller

The most critical bit of this I guess is the controller’s ‘GetDirectoryItems’ action. Here it is in all its glory…

@*---- 1 ----*@
private List<Dictionary<string, DirectoryElement>> GetDirectoryItems(IContentType contentType, JArray selectedProps, int currentPage, int itemsPerPage, string sortDir, string sortField)
{
List<Dictionary<string, DirectoryElement>> items = new List<Dictionary<string, DirectoryElement>>();
if (!CurrentPage.Children.Where(p => p.DocumentTypeAlias == contentType.Alias).Any())
{
 return items;
}

@*---- 2 ----*@
var pages = CurrentPage.Children((Func<IPublishedContent, bool>)(c => { return c != null && c.IsVisible() && c.DocumentTypeAlias == contentType.Alias; }));

@*---- 3 ----*@
pages = sortDir.Equals("asc", StringComparison.CurrentCultureIgnoreCase) ? pages.OrderBy(x => x.GetPropertyValue(sortField)) : pages.OrderByDescending(x => x.GetPropertyValue(sortField));

@*---- 4 ----*@
pages = pages.Skip((currentPage - 1) * itemsPerPage).Take(itemsPerPage);

@*---- 5 ----*@
foreach (IPublishedContent page in pages)
{
 Dictionary<string, DirectoryElement> item = new Dictionary<string, DirectoryElement>();

@*---- 6 ----*@
 foreach (JToken prop in selectedProps)
 {
  item[prop.Value<string>("alias")] = new DirectoryElement()
  {
   DirectoryValue = page.GetPropertyValue<string>(prop.Value<string>("alias")),
   DirectoryElementType = prop.Value<string>("propertyEditorAlias"),
   IsLinkToDetails = prop.Value<bool>("linkToDetails")
  };
}
items.Add(item);
}

return items;
}
  1. The items are returned as a List of Dictionary objects containing the property alias strings and DirectoryElements. If there are no children of the current page that match the correct document type, an empty list is returned
  2. The child pages are collected based on the selected document type
  3. If a sort order was set, the pages are sorted based on that field. One quite neat aspect of this is that the property can just be grabbed using .GetPropertyValue (notice no caste). I spent a while trying to work out how the property could be caste to the correct type dynamically. This is important since if the properties are all just caste to string’s, that works fine for sorting on text, but what about types like date/time? In the end I just tried it without casting the value and I guess it must be inferred somehow. Nice.
  4. The right load of pages are grabbed for the current page being viewed
  5. For each page, the properties of that page are gathered into Dictionary objects, using the property alias as the key and the value and type as the DirectoryElement value.

Conclusion

There’s still quite a bit to do here such as adding the search facility, making columns filterable and mapping other property types. I also thought that a prevalue could be added to the property editor itself so that only specific document types would show up in the list of allowed types.

As always comments and suggestions welcome.

Leave a Reply