On Creating an Orchard Module without Having to RTFM

Recently, I created my first Orchard module. Of course, rather than read the manual, I learned by copying the code for an existing plugin. So if you’re like me and you take a code-first approach to learning, read on as I’m going to document my experience below. Again, I haven’t RTFM (yet). So assume that some of what I’m writing is either incomplete or wrong. But the code works (or appears to be working). So I must have gotten something right…

The module I created was a wrapper around LinkedIn’s Member Profile widget. The module that I modeled my module after was the Facebook.Like module by the Orchard team. Both modules are basically wrappers around JavaScript includes.

I found it easiest to develop within the context of the Orchard source code, even though not entirely necessary. Either download the source or clone the repository. Setup an application in IIS that points to the Orchard.Web directory. After compiling the solution, you should be able to browse to Orchard and start setting up the site.

Modules are MVC apps. Start by creating an empty ASP.NET MVC 3 project, choosing Razor as the view engine. Save the new project to the Modules directory under the Orchard.Web directory. Clean out any of the superfluous scripts or styles. Add a reference to Orchard.Framework and Orchard.Core.

Basically, what my module needs to do is capture the five possible pieces of information that must be passed to the LinkedIn scripts:

  1. Public profile URL
  2. Display mode (inline, icon and name, icon)
  3. Display text (displayed with the icon and name mode)
  4. Display behavior (on click or on hover)
  5. Whether to show connections

So these five pieces of information will be used to create the model for the module. This model will be used to create the module’s backing table, the editor template and the module’s view.

The first piece to create in the module is the actual model class. In my case, the model is the Profile (Profile.cs in the Models directory). There are actually two models to create, one is a subclass of ContentPartRecord and one a subclass of ContentPart.

The ContentPartRecord is basically the class that represents the database schema. Again, I need to read more, but I can say from a couple of YSODs that a ProfilePartRecord class needs to have properties that match column names and all properties must be marked virtual. I believe the virtual requirement is related to NHibernate being used for data access, but it could be something else with DynamicProxy.

public class ProfilePartRecord : ContentPartRecord {

	public virtual string ProfileUrl { get; set; }
	public virtual string DisplayText { get; set; }
	public virtual string DisplayBehavior { get; set; }
	public virtual string DisplayMode { get; set; }
	public virtual bool ShowConnections { get; set; }

	public ProfilePartRecord() {
		DisplayBehavior = "hover";
		DisplayMode = "inline";
		ShowConnections = false;
	}
}

The ContentPart is essentially the view model for the module. It’s the class that gets bound to the editor template and the actual module view template. There won’t always be a one-to-one mapping between the ContentRecordPart and ContentPart, but in the case of my LinkedIn.Profile module, there was. However, as I write this explanation, I suspect I could/should change that correspondence. Note the use of the DataAnnotations validation attributes. The editor template will make use of these.

public class ProfilePart : ContentPart {
	
	[Required]
	public string ProfileUrl {
		get { return Record.ProfileUrl; }
		set { Record.ProfileUrl = value; }
	}

	[Required]
	public string DisplayText {
		get { return Record.DisplayText; }
		set { Record.DisplayText = value; }
	}

	[Required]
	public string DisplayBehavior {
		get { return Record.DisplayBehavior; }
		set { Record.DisplayBehavior = value; }
	}

	[Required]
	public string DisplayMode {
		get { return Record.DisplayMode; }
		set { Record.DisplayMode = value; }
	}

	public bool ShowConnections {
		get { return Record.ShowConnections; }
		set { Record.ShowConnections= value; }
	}
}

Once the model is created, the rest of the module dependencies can be filled in. The database migration probably makes sense as a next step. A Migrations.cs file at the root of the project contains the Migrations class, which subclasses DataMigrationImpl. If you’re not familiar with the concept of database migrations, I wrote about them in .NET a couple of years ago. Basically, you create a class that represents a schema change to the database. In the case of Orchard, Migrations use a Create method to add a table to the schema.

public class Migrations : DataMigrationImpl {

	public int Create() {

		SchemaBuilder.CreateTable("ProfilePartRecord", table => table
			.ContentPartRecord()
			.Column("ProfileUrl", DbType.String)
			.Column("DisplayText", DbType.String)
			.Column("DisplayBehavior", DbType.String)
			.Column("DisplayMode", DbType.String)
			.Column("ShowConnections", DbType.Boolean)                
		);

		ContentDefinitionManager.AlterPartDefinition(
			typeof(ProfilePart).Name, cfg => cfg.Attachable());

		return 1;
	}

	...
}

The table and column names must map to the model’s ContentPartRecord subclass. If you’re worried about collisions, you don’t need to. Tables are prefixed (with the namespace I think). I admit, I am not sure what the call to AlterPartDefinition with the lambda is doing, but it was in the Facebook.Like module. So I assume it’s necessary. Remember, I said I didn’t RTFM… I’m also not clear what the widget mapping in the migration does either, but again, it was in the Facebook.Like and appears to be meaningful. I’ll eventually look into these items.

   public int UpdateFrom1()
	{
		// Create a new widget content type with our map
		ContentDefinitionManager.AlterTypeDefinition("LinkedInProfileWidget", cfg => cfg
			.WithPart("ProfilePart")
			.WithPart("WidgetPart")
			.WithPart("CommonPart")
			.WithSetting("Stereotype", "Widget"));

		return 2;
	}

The next piece that needs to be included is the content part driver. Under Drivers, I have ProfileDriver.cs. ProfileDriver is a subclass of ContentPartDriver. Basically, this class is what gets data to the templates. ProfileDriver overrides Display and two Editor methods. In the Display method, the ProfilePart is mapped to a dynamic, which becomes the model for the module’s display view. The Editor methods provide a means for locating and displaying the editor template and saving changes, both within the context of the admin pages.

public class ProfileDriver : ContentPartDriver
{
	protected override DriverResult Display(ProfilePart part, string displayType, dynamic shapeHelper)
	{ 
		return ContentShape("Parts_Profile",
			() => shapeHelper.Parts_Profile(ProfileUrl: part.ProfileUrl,
											DisplayText: part.DisplayText,
											DisplayBehavior: part.DisplayBehavior,
											DisplayMode: part.DisplayMode,
											ShowConnections: part.ShowConnections));
	}

	protected override DriverResult Editor(ProfilePart part, dynamic shapeHelper)
	{
		return ContentShape("Parts_Profile_Edit",
			() => shapeHelper.EditorTemplate(
				TemplateName: "Parts/Profile",
				Model: part,
				Prefix: Prefix));
	}

	protected override DriverResult Editor(ProfilePart part, IUpdateModel updater, dynamic shapeHelper)
	{
		updater.TryUpdateModel(part, Prefix, null, null);
		return Editor(part, shapeHelper);
	}
}

In the Handlers namespace, the ProfileHandler extends ContentHandler. Content handlers define what happens to a content part during certain events, such as OnActivated and OnLoading. In the case of the ProfileHandler in my LinkedIn project, none of these events is used. The only code in the handler is that which adds the repository dependency to the Filters collection of the handler.

public class ProfileHandler : ContentHandler
{
	public ProfileHandler(IRepository repository)
	{
		Filters.Add(StorageFilter.For(repository));
	}
}

In terms of actual code, that’s it. The next step is to create the Razor templates. Under Views/EditorTemplates/Parts is Profile.cshtml. This is simply a CRUD form that will be loaded in the admin pages.

@model LinkedIn.Profile.Models.ProfilePart

LinkedIn Profile Widget
@Html.LabelFor(model => model.ProfileUrl)
@Html.TextBoxFor(model => model.ProfileUrl) @Html.ValidationMessageFor(model => model.ProfileUrl)
...
@Html.LabelFor(model => model.ShowConnections)
@Html.CheckBoxFor(model => model.ShowConnections) @Html.ValidationMessageFor(model => model.ShowConnections)

The Views/Parts/Profile.cshtml template is where the LinkedIn scripts are loaded in. Right now, I have the model setup to reflect the different settings, but some settings are mutually exclusive. Until I have a chance to update the UI to reflect these mutual exclusions, I have some hacky logic to determine which script to load. The logic is:

  • If the display mode is inline, everything shows and there is no need for hover or click behavior
  • Else If the display mode is icon, show the icon only (no name) and set hover or click behavior as set in the admin tools
  • Else same as Else If, but include the display text (typically person’s name)

This logic could be tighter and I’ll eventually update the ProfilePart to be more in line with how the scripts render. But for now, it’s simple and works.


@if(@Model.DisplayMode == "inline") {

} 
else if (@Model.DisplayMode == "icon") {
    
} else { 
          
}

The placement.info file in the root of the the project contains a chunk of XML used to place the Profile templates within the broader template structure of the site.


    
    

Finally, the Module.txt contains information used by the admin pages to link the module to the listing of installed modules. Here you define items like the author, dependencies, name and website for the module.

Name: LinkedIn.Profile
AntiForgery: enabled
Author: John Zablocki
Website: http://bitbucket.org/johnzablocki/orchardlinkedinprofile
Version: 0.1
OrchardVersion: 1.0
Description: This a module for using the LinkedIn Profile widget
Features:
    LinkedIn.Profile:
		Name: LinkedIn Profile
        Description: LinkedIn Profile widgets.
		Category: Widget
		Dependencies: Orchard.Widgets

If you have been playing along at home, you can now test out the module. After you’ve built the solution, you can navigate to your local site. In the admin dashboard, LinkedIn.Profile should show up on the list of installed modules (Installed tab of Modules page).

If you click the name of the module (LinkedIn.Profile) you should be taken to the Features tab of the Modules page, anchored at the LinkedIn.Profile module. Click enable.

Navigate next to Widgets. Click “Add” in any one of the content areas.

LinkedIn.Profile should be an option. Click through and you should see the form you created under EditorTemplates.

Fill out the values and view your home page. You should then see the LinkedIn Profile widget.

Well that’s an Orchard Module in a nutshell. This example is hardly the most complex, but it illustrates several basic concepts of putting a module together from scratch. I should also note that there are some command line tools included with Orchard to help automate some of the project setup. I think they’re part of the Web Platform Installer. Honestly, I haven’t tried them yet.

This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>