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 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.
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.
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.
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.
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.
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.
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. Filter
s 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); }
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>
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 Modules→Features, you’ll see the JWPlayer
enabled and uncategorized (Figure 6-1). We’ll fix the
metadata for our module a little later.
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 Modules→Features. 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.
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>
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).
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.
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.
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
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.