Building elements for Orchard layouts 2
In the previous post, I’ve shown how to build a simple layout element that was using the Forms API to build its editor. In this post, I’ll show a second element that doesn’t use the Forms API, but builds a custom editor instead.
This element displays information about a user content item. The element model itself just has the id of a user:
using Orchard.Layouts.Framework.Elements; using Orchard.Layouts.Helpers; using Orchard.Localization; namespace Decent.Sections.Elements { public class StaffMemberSection : Element { public override string Category { get { return "Sections"; } } public override string ToolboxIcon { get { return "\uf007"; } } public override LocalizedString Description { get { return T("Staff Member Section Element"); } } public int? UserId { get { return this.Retrieve(x => x.UserId); } set { this.Store(x => x.UserId, value);} } } }
The driver for the element this time looks a lot more like a part driver.
using System.Linq; using Decent.Sections.Elements; using Decent.Sections.ViewModels; using Orchard.ContentManagement; using Orchard.Data; using Orchard.Layouts.Framework.Drivers; using Orchard.Roles.Models; using Orchard.Users.Models; namespace Decent.Sections.Drivers { public class StaffMemberSectionElementDriver : ElementDriver<staffmembersection> { private readonly IContentManager _contentManager; private readonly IRepository<userrolespartrecord> _userRolesRepository; public StaffMemberSectionElementDriver( IContentManager contentManager, IRepository<userrolespartrecord> userRolesRepository ) { _contentManager = contentManager; _userRolesRepository = userRolesRepository; } protected override EditorResult OnBuildEditor( StaffMemberSection element, ElementEditorContext context) { var staffMemberIds = _userRolesRepository.Table .Where(ur => ur.Role.Name == "Staff") .Select(ur => ur.UserId); var staff = _contentManager .GetMany<userpart>(staffMemberIds, VersionOptions.Published, QueryHints.Empty) .ToDictionary(user => user.Id, user => user.UserName); var viewModel = new StaffMemberSectionViewModel { Staff = staff, UserId = element.UserId }; var editor = context.ShapeFactory.EditorTemplate( TemplateName: "Elements.StaffMemberSection", Model: viewModel); if (context.Updater != null) { context.Updater.TryUpdateModel(viewModel, context.Prefix, null, null); } return Editor(context, editor); } protected override void OnDisplaying( StaffMemberSection element, Orchard.Layouts.Framework.Display.ElementDisplayContext context ) { if (element.UserId == null) return; var userId = element.UserId.Value; context.ElementShape.UserId = userId; var staffMember = _contentManager.Get<userpart>(userId, VersionOptions.Published, QueryHints.Empty); context.ElementShape.User = staffMember; } } }
Instead of building a form, it has an OnBuildEditor method that builds a view model with the full list of staff members (we’ll build a drop-down with this in the editor template), then prepares an editor template, and updates the view model if necessary. The view model looks like this, a couple of properties for the user id and the list of staff members:
using System.Collections.Generic; namespace Decent.Sections.ViewModels { public class StaffMemberSectionViewModel { public int? UserId { get; set; } public Dictionary<int, string> Staff { get; set; } } }
The editor template (/Views/EditorTemplates/Elements.StaffMemberSection.html) is very simple: it’s just a label and a drop-down built from the Staff dictionary that’s on the view model:
@using System.Globalization @model Decent.Sections.ViewModels.StaffMemberSectionViewModel <fieldset> @Html.LabelFor(m => m.UserId, T("Staff Member")) @Html.DropDownListFor(m => m.UserId, Model.Staff.Select(kvp => new SelectListItem { Value = kvp.Key.ToString(CultureInfo.InvariantCulture), Text = kvp.Value, Selected = kvp.Key == Model.UserId })) </fieldset>
This is it for the editor. The OnDisplaying method of the driver fetches the user from the database and sticks it on the element shape. The front-end template (/Views/Elements/StaffMemberSection.cshtml) gets the user content item from Model.User, then displays that. On that particular site, I’ve added a number of fields to the user content type, including media picker fields for portraits of the staff members. That’s what you’re seeing being initialized at the top of the file:
@using Orchard.Layouts.Helpers @{ var member = Model.User; var memberItem = member.ContentItem.User; var userName = member.ContentItem.UserPart.UserName; var firstName = memberItem.FirstName.Value; var lastName = memberItem.LastName.Value; var portraitField = memberItem.Portrait; var portraitMedia = portraitField == null ? null
: (dynamic)((IEnumerable<object>)portraitField.MediaParts).FirstOrDefault(); var portraitUrl = (portraitMedia == null ? null
: portraitMedia.MediaUrl) ?? "/Themes/LesLilas/Content/void.gif"; var position = memberItem.Position.Value; var bio = memberItem.Biography.Value; var facebook = memberItem.Facebook.Value; var linkedin = memberItem.LinkedIn.Value; var twitter = memberItem.Twitter.Value; var tagBuilder = TagBuilderExtensions.CreateElementTagBuilder(Model, "section"); tagBuilder.Attributes["id"] = userName; tagBuilder.AddCssClass("about-2"); } @tagBuilder.StartElement <div class="container"> <div class="row"> <div class="col-lg-4 col-lg-offset-1"> <img src="@portraitUrl" alt="@firstName @lastName"
class="img-circle img-responsive img-centered dark-faded-border"> </div> <div class="col-lg-5 text-center"> <h2>@firstName @lastName</h2> <hr class="primary"> <h3>@position</h3> <p>@Html.Raw(bio)</p> <ul class="list-inline"> @if (facebook != null) { <li> <a class="btn btn-social-dark btn-facebook"
href="https://www.facebook.com/@facebook"><i
class="fa fa-facebook fa-fw"></i></a> </li> } @if (linkedin != null) { <li> <a class="btn btn-social-dark btn-linkedin"
href="https://www.linkedin.com/@linkedin"><i
class="fa fa-linkedin fa-fw"></i></a> </li> } @if (twitter != null) { <li> <a class="btn btn-social-dark btn-twitter"
href="https://twitter.com/@twitter"><i
class="fa fa-twitter fa-fw"></i></a> </li> } </ul> </div> </div> </div> @tagBuilder.EndElement
This element was a little more complex than the subscription element from the first part of this series, as it takes things into its own hands more. This should show how Orchard layouts lets you take over just as much as you need. Next time, we’ll go even farther and build an element that describes a list of talking points.