Chapter 6. Creating Widgets

In Chapter 5, we walked through the process of creating a content field. In order to be used, our field had to be added to a content type. We couldn’t just add it to a zone in a template and have it appear. For that behavior, we’ll need a widget. Like fields, widgets are a type of Orchard module. In this chapter, we’ll walk through the process of creating a widget that we can attach to all or some pages in our site.

Content Parts

Content parts are reusable pieces of functionality (UI or behavior) that are added to content types. When we created the Event content type, we added the Containable content part to its definition, which allowed events to be contained in projection pages. The Body content part allowed us to include an HTML body in our Bio content type.

Even though a widget is a content type and not a part, creating a widget is effectively the same process as creating a content part. However, we’ll include some additional metadata to make the part behave like a widget and not a part. In other words, once a part becomes a widget we can add it as content in a zone.

In this chapter, we’re going to create a module for embedding videos on our site. At the time of this writing, there exists only a single YouTube module in the Orchard Gallery. That module is a field, which again requires a content type to be defined with that field in order for it to be used with content items. What we want instead is a widget that we can attach to different pages, regardless of the content type for those pages.

The JW Player Widget

YouTube offers a very simple interface for embedding its videos into other sites. We could easily create a YouTube video widget that does nothing more than replace HTML iframe tag attribute values (i.e., should precede src, height, width) with our video’s details. However, that approach would be a bit limiting in that we might want to host our own videos or pull in content from video sites other than YouTube.

Instead of creating a standalone YouTube module, we’re going to create a widget that uses the JW Player for Flash and HTML5 for media rendering. The JW Player is an extensible media player that uses Flash and JavaScript to provide support for skinning, plugins, playlists, and customizable controls. Our widget will provide two services for sites that use it. It’ll package up the JavaScript and Flash files and build the JavaScript to embed on our pages.

Creating the Module

Once again we’ll return to PowerShell (or the standard command line) to create an Orchard session. If you don’t have an Orchard session open, navigate to the Orchard.Web project’s bin directory. Once there, execute Orchard.exe to initiate the session. We’re going to use the code generation tools to create the skeleton for our module project. Though our widget will be decidedly different than our field, the starting project should look similar to what we saw when we used the codegen tools to create our Contrib.PlacesField project:

orchard> codegen module JWPlayer
Creating Module JWPlayer
Module JWPlayer created successfully

As was the case with our places field from Chapter 5, the solution will require a reload after you run this command. Once reloaded, you’ll see the skeleton of a module project under the Modules solution folder. If you expand it, you’ll again see the placeholder directories that we saw in the last chapter.

The process of creating a widget is not unlike creating a field, but the supporting infrastructure for a widget module is more complex and will require a few additional steps. As we did with our field module, we’re going to start out by considering our model classes.

The JW Player supports many customizable features, from simply setting the media file to be played to setting behaviors such as whether to auto-start or repeat. To keep our module from getting too complex, we’ll pare down the features to a minimal, yet still interesting set. We’ll need the obvious properties for filename, width, and height. We’ll also support the behaviors mentioned previously. Beyond that, you could visit http://www.longtailvideo.com to see the additional properties that could be included.

Note

The JW Player is an open source project that is free to use for non-commercial sites. If you’re interested in using this module or the player on a commercial site, you will need to purchase a commercial license from LongTail Video.

JW Player Model

The codegen utility created a directory named Models in our new module project. We’re going to add two files to this directory. Start by creating a file named JWPlayerPartRecord.cs. This is the class that will be used by Orchard to create the database records for our widget. It’s basically a POCO with properties that will map to the columns in our widget’s table. Each of these properties represents a piece of data that we’ll collect from content creators who use our widget. We’ll see how the data model comes together shortly:

using Orchard.ContentManagement.Records;

namespace JWPlayer.Models {
    public class JWPlayerPartRecord : ContentPartRecord {
        public virtual string PlayerSource { get; set; }

        public virtual int Height { get; set; }

        public virtual int Width { get; set; }

        public virtual string MediaFile { get; set; }

        public virtual bool AutoStart { get; set; }

        public virtual bool Repeat { get; set; }
    }
}

There are two important pieces to this class. The first is that it extends Orchard’s ContentPartRecord class, which simply provides its subclasses with an Id property. The second is that all properties are marked virtual. The reason for this modifier is that Orchard uses the object relational mapper (ORM) NHibernate for data access. NHibernate will dynamically generate a proxy class based on ContentPartRecord classes and requires virtual properties to be able to do so.

Next, we’re going to create a class that is similar to the ViewModel class we created for our field in the previous chapter. It’s similar in that it will be the class that’s used to bind to admin forms. Unlike our ViewModel, though, this class will have a formal requirement of extending Orchard’s ContentPart class. Add a new file to the Models directory named JWPlayerPart.cs:

using System.ComponentModel.DataAnnotations;
using Orchard.ContentManagement;

namespace JWPlayer.Models {
    public class JWPlayerPart : ContentPart<JWPlayerPartRecord> {

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

        [Required]
        public int Height {
            get { return Record.Height; }
            set { Record.Height = value; }
        }

        [Required]
        public int Width {
            get { return Record.Width; }
            set { Record.Width = value; }
        }

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

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

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

    }

}

It’s common, though not required, that this new ContentPart class will simply map each of the properties of the ContentPartRecord to a property with the same name. In our case, we’re simply going to ask for and receive input values that correspond to a column in our widget’s table. If our UI was required to be more complex, then our properties might not map directly to the properties. Notice, too, that we’re including validation attributes on our properties. Orchard’s admin pages will automatically enforce these rules when attempting to save new instances of our widget.

Database Migrations

We’ve discussed that NHibernate is used for data access and will use our JWPlayerPartRecord class to store and retrieve the data for our widget. However, we haven’t actually seen how that table gets created. To create our table, we’re going to need to create a data migration.

Data migrations are a concept borrowed from frameworks such as Ruby on Rails. The basic idea is that we version our database through a series of migration scripts. Each modification to the database (new table, dropped column, etc.) is a new version of the database. Each version has a migration associated with it, which is either a SQL script or a migration class that often uses some sort of domain specific language (DSL).

In our case, we’re going to version modules and not the entire database. You’ll define a schema version for your module and move it up to newer versions. This versioning will be done through both convention and a simple DSL implemented by the Orchard framework. Return to your command-line Orchard session and run the following command:

orchard> codegen datamigration JWPlayer
Creating Data Migration for JWPlayer
Data migration created successfully in Module JWPlayer

As it did when we created the JWPlayer module, the codegen utility will force a solution reload. What’s been added is a new file named Migrations.cs to the root of our JWPlayer module project (for which you should “Include In Project”). This generated file contains a class named Migrations with a single method named Create. When we eventually enable this module, Orchard will execute this method to create the initial schema for our module:

public class Migrations : DataMigrationImpl {

  public int Create() {

        // Creating table JWPlayerPartRecord
        SchemaBuilder.CreateTable("JWPlayerPartRecord", table => table
             .ContentPartRecord()
             .Column("PlayerSource", DbType.String)
             .Column("Height", DbType.Int32)
             .Column("Width", DbType.Int32)
             .Column("MediaFile", DbType.String)
             .Column("AutoStart", DbType.Boolean)
             .Column("Repeat", DbType.Boolean)
        );

        ContentDefinitionManager.AlterPartDefinition("JWPlayerPartRecord",
          builder => builder.Attachable());

        return 1;
  }
}

Our migration class extends the base migrations class, which provides access to the SchemaBuilder property. SchemaBuilder, which is an instance of Orchard’s SchemaBuilder class, contains methods for creating our migrations (creating tables, dropping columns, adding foreign keys, etc.).

The codegen utility created a table definition for us based on our JWPlayerPartRecord property names. We also need to add the AlterPartDefinition call to tell Orchard that our content part may be attached to any content type. By convention, Create will be the method called when Orchard runs our module’s migration for the first time (on install and enable). Create returns 1, which sets the current schema version for our module to 1. We’ll see shortly how a second version is created and executed.

Handlers and Drivers

During our exploration of themes and modules we’ve seen analogies to ASP.NET MVC. When writing content parts, these analogies still hold. In ASP.NET MVC, we can use Filter classes to hook into the execution of a request. Filters provide a mechanism for executing code before and after a request. Similarly, with our content parts, we can create a ContentHandler subclass that offers us the opportunity to hook into different points in our module’s execution.

Create a new directory named Handlers to our new module project, and add to it a file named JWPlayerHandler.cs. While we could create a handler that overrides lifecycle events such as OnCreating, OnCreated, OnActivating, or OnActivated, our needs are simple. We’ll include only a constructor with some plumbing code to instruct Orchard to wire up data access for our module using our ContentPartRecord implementation:

using JWPlayer.Models;
using Orchard.Data;
using Orchard.ContentManagement.Handlers;

namespace JWPlayer.Handler {
    public class JWPlayerHandler : ContentHandler {

        public JWPlayerHandler(IRepository<JWPlayerPartRecord> repository) {
            Filters.Add(StorageFilter.For(repository));
        }

    }

}

Like content fields, content parts will use driver classes to take care of the actual rendering of views and processing of admin data collection. Create a new directory named Drivers and add to it a class named JWPlayerDriver.cs:

using Orchard.ContentManagement.Drivers;
using JWPlayer.Models;
using Orchard.ContentManagement;

namespace JWPlayer.Drivers
{
    public class JWPlayerDriver : ContentPartDriver<JWPlayerPart> {
      //methods shown below
    }
}

The Display method will be used to render our widget in a zone. The ContentShape method will construct a shape to be bound to our view file. Our shape will simply have properties that map one-to-one to our JWPlayerPart class. Orchard will call the Display method when a content item is rendered that uses our widget:

protected override DriverResult Display(JWPlayerPart part,
        string displayType, dynamic shapeHelper) {

  return ContentShape("Parts_JWPlayer",
                   () => shapeHelper.Parts_JWPlayer(
                          MediaFile: part.MediaFile,
                          PlayerSource: part.PlayerSource,
                          Width: part.Width,
                          Height: part.Height,
                          AutoStart: part.AutoStart,
                          Repeat: part.Repeat));

}

The first Editor method is used to load the admin template for collecting the data that we’ll use to configure our JWPlayer widget. Using the EditorTemplate method of the dynamic shapeHelper, we instruct Orchard on how to locate our editor template view file and to use a JWPlayerPart instance as the model for this view:

protected override DriverResult Editor(JWPlayerPart part,
                             dynamic shapeHelper) {

  return ContentShape("Parts_JWPlayer_Edit",
                        () => shapeHelper.EditorTemplate(
                             TemplateName: "Parts/JWPlayer",
                             Model: part,
                             Prefix: Prefix));
}

The second Editor method is used to handle the save action when our widget is configured in the Dashboard. The JWPlayerPart parameter is bound to the values from the form and saved back to the database. After saving, the editor form is redisplayed, with the updated JWPlayerPart instance as its model:

protected override DriverResult Editor(JWPlayerPart part,
                   IUpdateModel updater,
                   dynamic shapeHelper) {

  updater.TryUpdateModel(part, Prefix, null, null);
  return Editor(part, shapeHelper);
}

Placement

Next, we’re going to need to create our Placement.info file in the root of the project. Again, placement files are used by Orchard to determine where to display a widget (or other content types) relative to other widgets, fields, and parts. Review Creating Content or Creating Modules for more on Placement.info:

<Placement>
  <Place Parts_JWPlayer="Content:3"/>
  <Place Parts_JWPlayer_Edit="Content:3"/>
</Placement>

Enabling Our Module

We haven’t finished our widget yet, but we’ve created enough of our project to test out the basic correctness of our plumbing. Make sure you compile your project and then return to the command line:

orchard> feature enable JWPlayer
Enabling features JWPlayer
JWPlayer was enabled

By enabling the feature, you’ve run the data migration. If you open the Dashboard and select ModulesFeatures, you’ll see the JWPlayer enabled and uncategorized (Figure 6-1). We’ll fix the metadata for our module a little later.

The enabled and uncategorized JWPlayer module

Figure 6-1. The enabled and uncategorized JWPlayer module

A Second Migration

If you click “Widgets” and then click “Add” on any of the zones, you might expect that you’d see our new widget in the list of possible choices, but it doesn’t appear. We’re going to fix that problem now. We’ll need to modify Migrations.cs to include an update to our module that will allow it to behave as a widget:

public int UpdateFrom1() {

  ContentDefinitionManager.AlterTypeDefinition(
        "JWPlayerWidget", cfg => cfg
        .WithPart("JWPlayerPart")
        .WithPart("WidgetPart")
        .WithPart("CommonPart")
        .WithSetting("Stereotype", "Widget"));

  return 2;
}

Notice first, that the method name is UpdateFrom1. By convention, Orchard will know to run this method if the current schema version for our module is 1 (which it is after running Create). This method creates a new widget type by composing a new type from JWPlayerPart, WidgetPart, and CommonPart. In other words, the definition of our widget has properties from each of these three parts. Conceptually, this is similar to adding parts to our Event or Bio content types.

Compile your migration and return to the admin dashboard. Navigate to ModulesFeatures. You won’t see any notification that a migration needs to be run for the JWPlayer module. That’s because when Orchard detects that there’s a new migration to run for a module, it runs it automatically. While this might sound a little scary, keep in mind that in all likeliness you would explicitly and manually install a newer version of the module. Orchard is just taking care to make sure the code and data are in sync.

The Widget Views

Before we try to add our widget to a zone, we first have to create the views for it. Our widget is going to encapsulate the third-party JWPlayer media player. As you might therefore expect, we’re going to need to include some third-party files. Download the latest JWPlayer from http://www.longtailvideo.com/players/jw-flv-player/.

You’re going to need to put a couple of files into your module project from the ZIP file you downloaded. Copy jwplayer.js into the Scripts directory. Create a new directory named Flash and add the player.swf file. You should also include both in your project from the Solution Explorer. You’ll also need to copy the web.config file from the Scripts directory into the Flash directory so the static player.swf file will be property served. I’m also going to add the license.txt file to the root for good measure.

Next, under the Views directory, create a Parts directory and a new file under that directory named JWPlayer.cshtml. This file will first include the jwplayer.js JavaScript file and then include a script block that will configure the player. The properties we set in the admin tool are embedded within this JavaScript in order to display the player as our content creators will specify:

@using Orchard.UI.Resources
@{ Script.Include("jwplayer.js")
        .AtLocation(ResourceLocation.Head); }
<div id='mediaspace'>This text will be replaced</div>

<script type='text/javascript'>
    jwplayer('mediaspace').setup({
        'flashplayer': '@Url.Content(Model.PlayerSource)',
        'file': '@Url.Content(@Model.MediaFile)',
        'autostart': '@Model.AutoStart',
        @if (Model.Repeat) {
        <text>'repeat' : 'always',</text>
        }
        'width': '@Model.Width',
        'height': '@Model.Height'
    });
</script>

In this view, we included the jwplayer.js script by using the Include method of our view’s Script property. If instead we wanted to use the Require method to guarantee the script will load only once per page view, we need to create a class that implements IResourceManifestProvider. Create a new file named ResourceManifest.cs at the root of the module:

using Orchard.UI.Resources;

namespace JWPlayer {
    public class ResourceManifest : IResourceManifestProvider {

        public void BuildManifests(ResourceManifestBuilder builder) {
            var manifest = builder.Add();
            manifest.DefineScript("jwplayer").SetUrl("jwplayer.js");
        }
    }

}

After creating this file, we can now include our script file using the Require method, replacing the Include method we originally used previously:

@{ Script.Require("jwplayer").AtHead(); }

We also need to create our admin UI. Create a new directory named EditorTemplates under Views, and under that new directory create one named Parts. In the new Parts directory, create a file named JWPlayer.cshtml. This new view will be a simple editor form with fields for each of our JWPlayerPart properties. Again, because we decorated these properties with Required attributes from System.ComponentModel.DataAnnotations, we’re able to include validation messages as well:

@model JWPlayer.Models.JWPlayerPart

<fieldset>
  <legend>JWPlayer Widget</legend>

  <div class="editor-label">
    Player Source
  </div>
  <div class="editor-field">
    @Html.TextBoxFor(model => model.PlayerSource)
    @Html.ValidationMessageFor(model => model.PlayerSource)
  </div>

  <div class="editor-label">
    Height
  </div>
  <div class="editor-field">
    @Html.TextBoxFor(model => model.Height)
    @Html.ValidationMessageFor(model => model.Height)
  </div>

  <div class="editor-label">
    Width
  </div>
  <div class="editor-field">
    @Html.TextBoxFor(model => model.Width)
    @Html.ValidationMessageFor(model => model.Width)
  </div>

  <div class="editor-label">
    Media File
  </div>
  <div class="editor-field">
    @Html.TextBoxFor(model => model.MediaFile)
    @Html.ValidationMessageFor(model => model.MediaFile)
  </div>

  <div class="editor-label">
    Auto Start
  </div>
  <div class="editor-field">
    @Html.CheckBoxFor(model => model.AutoStart)
  </div>

  <div class="editor-label">
    Repeat
  </div>
  <div class="editor-field">
    @Html.CheckBoxFor(model => model.Repeat)
  </div>

</fieldset>

Adding the Widget to a Zone

We’re now ready to add this widget to a zone. We’ll add this widget to the AsideSecond zone, where we currently have our “Upcoming Events” list. We’ll also add a layer rule so that it appears only on the events page. We’ll use this new widget to display videos from past events.

Return to the Dashboard and click “Widgets.” Click the “Add new layer...” link. Name the new layer “Events” and add the rule url '~/events' and save. With the “Events” layer selected, click “Add” on the AsideSecond zone. You should now see the JWPlayer Widget (Orchard attempts to make names more readable, hence the space between the “J” and “W”) as an option (Figure 6-2).

The new JWPlayer widget option

Figure 6-2. The new JWPlayer widget option

Click the “J W Player Widget” option and you’ll see the admin form we just created (Figure 6-3). Enter a height and width of 200 and 300 respectively. Set the “Player Source” to “~/Modules/JWPlayer/Flash/player.swf” and the “Media File” to “http://youtu.be/tyHwA7IyjuY” without the quotes. Enter the title “Daisy’s Videos.” Click Save and browse to the Events page (Figure 6-4). If you see an error that it can’t find the player, rebuild your project. You’ll now see a video player embedded into the AsideSecond zone.

Note

If you are serving a video through your local IIS and getting a 404 error, the MIME type might not be mapped. To serve video files, such as *.mp4 files via IIS, open up the IIS property pages for MIME Types and add *.mp4 with a mime type of video/mp4.

Widget Metadata

Finally, we’ll want to update our Module.txt file so that the metadata used by the Dashboard correctly represents our work:

Name: JWPlayer
AntiForgery: enabled
Author: John Zablocki
Website: http://dllhell.net
Version: 1.0
OrchardVersion: 1.4
Category: Media
Description: JW Flash Player Widget
Features:
    JWPlayer:
        Description: JW Flash Player Widget
The JWPlayer widget admin form

Figure 6-3. The JWPlayer widget admin form

The finished JWPlayer widget

Figure 6-4. The finished JWPlayer widget

Summary

We’ve now seen how to create two types of modules in Orchard: fields and parts (by way of a widget). Fields offer granular levels of functionality that may be used when creating content types. Parts are similar to fields, but may encapsulate more features, such as the ability to be composed into a widget. We’ve only touched on some of what modules can do. In fact, there are Orchard modules that are responsible for creating and managing our Orchard modules.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset