Chapter 5. Creating Modules

We’ve already seen that Orchard may be extended easily and quickly by installing modules from the Gallery. When we wanted to add an image field to our event pages, we just installed the ImageField module. When we didn’t want to create our own contact form, we just installed one that someone had already written and shared. So what happens when we want functionality that isn’t readily available in the Gallery? Well, we just have to trade instant gratification for a little elbow grease.

The Places Field

Some of the most interesting data sources available to developers are those that contain places information. These are the databases that allow you to check into a business or some other venue with Facebook or Foursquare. When you arrive somewhere, your phone (or HTML5 capable browser) sends your location to a server-side API where a list of businesses, restaurants, and other public points of interest is then returned to the phone. You select the one where you are and check in.

While it’s unlikely that users will be checking into the Daisy’s Gone site, there’s another use of places data of which we might wish to take advantage. Consider the case of Meetup.com, where users who want to schedule a Meetup for their group are asked to select a location before saving the new event. The location is chosen from an auto-suggested list of places, similar to the check-in features of Foursquare and Facebook.

This is the functionality we’re going to add to our Event content type. Currently, our Location field is a text field into which event creators enter free-form text. In order to limit the locations to valid venues, we’ll instead use a field that auto-suggests possible places given a center point. To get this functionality, we’ll need a module. Fortunately for us, as of this writing, there is no such module in the Orchard Gallery. I say “fortunately” because we’re going to have the opportunity to create it in this chapter.

Getting Places Data

There are several places where we could get our places data. Facebook and Foursquare provide places data that is somewhat coupled with their respective services. Google provides places data, but doesn’t allow that data to be rendered on a Bing Map (or any other non-Google Maps service). SimpleGeo provides places data, but it requires a paid developer key. For our needs, Yelp provides an API that will suit us well.

Yelp offers a REST API that is easily consumed from a .NET application. For our project, we’ll use the YelpSharp library that’s available on Nuget. We’re not really concerned with the inner workings of a RESTful API client, so we’ll just install the client and use it as a black box.

Module Code Generation

When we created our new theme in Chapter 4, we used the Orchard.CodeGeneration module to create the initial directory structure for our theme’s files. The new files were added to the Themes project that is part of the main Orchard solution. Unlike themes, each module is housed in its own project by default. Within the solution, these projects are organized under the “Modules” solution folder. On disk, these projects are stored in the Modules directory beneath the Orchard.Web directory.

Running the code generation command to create a new module will result in a new C# project being added to the solution within the Modules solution folder. If you’re familiar with ASP.NET MVC, you’ll probably recognize the structure, with familiar directories such as Views and Controllers. As we build out our module we’ll explore the project in more detail.

Note

Unlike the other modules that typically ship with Orchard, the code generated module projects are not web projects, but rather standard class library projects. What this difference means is that some of the context menus will not be web-centric (i.e., there is no “Add Controller”).

As we did when creating our theme, we’ll use PowerShell (or the standard command line) to create an Orchard session. If you closed out your session from the previous chapter, navigate to the Orchard.Web project’s bin directory. Once there, execute Orchard.exe to initiate the session:

orchard> codegen module Contrib.PlacesField
Creating Module Contrib.PlacesField
Module Contrib.PlacesField created successfully

The default behavior of the codegen utility is to add the new module project to the solution. So when you return to your solution in Visual Studio, you’ll be prompted to reload. Once you do, your new module project will be available. The module project is mostly comprised of empty directories and a few web.config files at this point. There’s also a Module.txt file that will describe our module to the admin Dashboard. Again, we’ll explore these project files as we start building out our new module.

We’re going to create a content field, which is a little different than creating a more complex content part. Each has its own conventions. We’ll examine content part creation in the next chapter on our way to creating a widget. However, some of the steps and concepts we’ll use while creating our field will certainly provide us with transferable knowledge when it comes time to create a more complex module.

The Places Field Project

Before we add any code to the project, let’s review the goal of our field to make sure we understand its requirements. At a high level, we simply want to limit the list of locations for an event to those that come from the Yelp API. To do so, we’ll ask admin users (or content creators) to provide some details that will narrow the places search. After providing those pieces of information, users may then select a place from the list. That place and its details will then be saved and made available for rendering on the event pages (or any other content items where this field is used).

The Yelp API is highly flexible, allowing for searches by category, distance, search terms, and more. For the sake of keeping things simple our field won’t support all possible query options, though it should be sufficiently generic to allow for reuse.

Places Field Model

We’ll start off by considering the model for our field. Our model will represent the query and its result. To that end, we’ll create a model class that has properties for some of the possible search parameters that we’ll use when querying the API. This model class will be a subclass of Orchard’s ContentField, which will provide our class with a few basic services that we’ll see as we build our field.

Under the new Contrib.PlacesField project, create a new directory named Fields and add a class to that directory named PlacesField.cs. This class is just a plain old CLR object (POCO) that uses the persistence capabilities of the Storage property from its base class to save and retrieve the values set by content creators:

using Orchard.ContentManagement;

namespace Contrib.PlacesField.Fields {

    public class PlacesField : ContentField {

        public string PostalCode {
            get { return Storage.Get<string>("PostalCode"); }
            set { Storage.Set<string>("PostalCode", value); }
        }

        public string Category {
            get { return Storage.Get<string>("Category"); }
            set { Storage.Set<string>("Category", value); }
        }

        public string PlaceName {
            get { return Storage.Get<string>("PlaceName"); }
            set { Storage.Set<string>("PlaceName", value); }
        }

        public string PlaceLatLong {
            get { return Storage.Get<string>("PlaceLatLong"); }
            set { Storage.Set<string>("PlaceLatLong", value); }
        }
    }

}

In our admin pages, we’re going to collect information that is specific to a content item that is using our field, namely a postal code, category, and the selected place. We’re also going to collect settings from content type creators that will affect how all content items built from a content type definition behave. We’ll create the data model for our settings shortly and see how this all ties together when we update our Event type.

We’ve just defined a mixed data model that combines type-level settings with item-level data. The settings and data will both be used to render places by changing the behavior of the view appropriately. To bridge the gap between our disparate models and the denormalized view, we’ll create a class that knows about both but more closely resembles the view. This approach is commonly known as the Model-View-ViewModel (MVVM) pattern, and it is frequently used in ASP.NET MVC applications.

Create a new folder named ViewModels and add a file named PlacesFieldViewModel.cs. This class will look very similar to our actual model class, but it won’t concern itself with persistence. It will also have a simple string property for our category IDs and two additional properties for keeping track of our view options (the global settings):

namespace Contrib.PlacesField.ViewModels {

    public class PlacesFieldViewModel {

        public string Name { get; set; }

        public string PostalCode { get; set; }

        public string Category { get; set; }

        public string PlaceName { get; set; }

        public string PlaceLatLong { get; set; }

        public bool ShowLink { get; set; }

        public bool ShowMap { get; set; }

    }

}

As hinted at, we’re also going to include an option that will allow admins to decide which way to render the selected place on content pages. We’ll provide options for displaying only the name of the place, displaying the name with a link to Bing Maps, or with an embedded Bing Map (using an iframe element, not the widget).

The way we’ll represent these options in our model will be to create a settings class. This class is just another POCO and will be used by the admin pages that manage content types that use our field. We’ll include a simple enumeration for the options. Create a new folder named Settings and add a file PlacesFieldSettings.cs with the following content (you can include the enum definition in this file):

namespace Contrib.PlacesField.Settings {

    public enum PlacesFieldDisplayOptions {
        NameOnly,
        NameAndLinkToMap,
        NameAndEmbeddedMap
    }

    public class PlacesFieldSettings {

        public PlacesFieldDisplayOptions DisplayOptions { get; set; }

    }

}

Drivers

Now that we’ve created our supporting data classes, it’s time to write some code to take care of the actual rendering and saving of our field data. The class that has these responsibilities is known as a “driver.” We’ll also see drivers when we build a content part in Chapter 6.

If you’re familiar with ASP.NET MVC, you could think of drivers as being analogous to controller classes that are tightly coupled to a module. Drivers will be responsible for rendering our field’s admin (when creating content) and content item (when displaying content) templates.

Create a new directory named Drivers and add a file named PlacesFieldDriver.cs. This new class will extend ContentFieldDriver, which will allow Orchard to recognize that this module is in fact a content field. There are three methods we’ll override in this class, which are discussed next. Though it certainly looks complex, the Display and Editor methods are simply either passing data to a view or reading data back from a form post:

using Orchard.ContentManagement.Drivers;
using Contrib.PlacesField.Settings;
using Orchard.ContentManagement;
using Contrib.PlacesField.ViewModels;

namespace Contrib.PlacesField.Drivers {

    public class PlacesFieldDriver : ContentFieldDriver<Fields.PlacesField> {
          //methods shown below
    }

}

The Display method begins by retrieving the saved settings for our field (we’ll write code to save these shortly). The ContentField class that we extended to create our PlacesField provides the functionality to retrieve these settings. After getting the settings, the ContentShape method is used to return a shape to the view that will be used for display.

Note that this code won’t compile until we complete the rest of the dependent code we write later in this chapter, so don’t worry if you see several missing references.

Recall that the content that we bind to our views is rendered as a special dynamic type called a shape. The lambda expression provided as our third argument to ContentShape is creating the actual shape that will be used as the model for our PlacesField view:

protected override DriverResult Display(ContentPart part,
                                            Fields.PlacesField field,
                                            string displayType,
                                            dynamic shapeHelper) {

    var settings = field.PartFieldDefinition
                                .Settings
                                .GetModel<PlacesFieldSettings>();

    return ContentShape("Fields_Contrib_Places",
               field.Name,
               s => s.Name(field.Name)
               .PlaceName(field.PlaceName)
               .PlaceLatLong(field.PlaceLatLong)
               .ShowLink(settings.DisplayOptions ==
                     PlacesFieldDisplayOptions.NameAndLinkToMap)
               .ShowMap(settings.DisplayOptions ==
                     PlacesFieldDisplayOptions.NameAndEmbeddedMap)
               );
}

Note

C# 4.0 introduced the dynamic keyword, which when applied to a variable, instructs the compiler not to check for compile-time correctness of properties and methods. In other words, you are letting the compiler know that everything will evaluate correctly at runtime. Orchard makes heavy use of this dynamic feature (e.g., shapes). Without C# 4.0, Orchard could not exist as it’s written today.

The first Editor method is used to load our field into the dashboard editor forms for content types using our field (i.e., CreateNewEvent). As was the case with Display, Editor also begins by retrieving the saved settings. It then composes a ViewModel instance from both settings and persisted field values. ContentShape is again called, but in this case, the EditorTemplate method of the dynamic shapeHelper is used to load our editor template and provide our ViewModel instance as the view’s Model property:

protected override DriverResult Editor(ContentPart part,
                                          Fields.PlacesField field,
                                          dynamic shapeHelper) {

    var settings = field.PartFieldDefinition
                     .Settings.GetModel<PlacesFieldSettings>();
    var viewModel = new PlacesFieldViewModel {
          Name = field.Name,
          Category = field.Category,
          PostalCode = field.PostalCode,
          ShowLink = settings.DisplayOptions
               == PlacesFieldDisplayOptions.NameAndLinkToMap,
          ShowMap = settings.DisplayOptions
               == PlacesFieldDisplayOptions.NameAndEmbeddedMap,
          PlaceName = field.PlaceName,
          PlaceLatLong = field.PlaceLatLong
    };

    return ContentShape("Fields_Contrib_Places_Edit",
                     () => shapeHelper.EditorTemplate(
                     TemplateName: "Fields/Contrib.Places",
                     Model: viewModel,
                     Prefix: getPrefix(field, part)
                     ));
}

The second Editor method handles the post-back for our field when a user saves a content item that was defined to use our new field. After saving changes and mapping posted values to our field instance, the edit template is redisplayed (by way of the content item’s editor form):

protected override DriverResult Editor(ContentPart part,
                           Fields.PlacesField field,
                           IUpdateModel updater,
                           dynamic shapeHelper) {

    var viewModel = new PlacesFieldViewModel();

    if (updater.TryUpdateModel(viewModel,
                     getPrefix(field, part), null, null)) {

        var settings = field.PartFieldDefinition
              .Settings.GetModel<PlacesFieldSettings>();


        field.Category = viewModel.Category;
        field.PostalCode = viewModel.PostalCode;
        field.PlaceName = viewModel.PlaceName;
        field.PlaceLatLong = viewModel.PlaceLatLong;

        viewModel.ShowLink = settings.DisplayOptions
            == PlacesFieldDisplayOptions.NameAndLinkToMap;
        viewModel.ShowMap = settings.DisplayOptions
            == PlacesFieldDisplayOptions.NameAndEmbeddedMap;

    }

    return Editor(part, field, shapeHelper);
}

The private getPrefix method is used to create unique column values in the database for our field. It’s a convention to define this method, not a requirement:

private static string getPrefix(ContentField field,
                                     ContentPart part) {
       return (part.PartDefinition.Name + "." + field.Name)
               .Replace(" ", "_");
}

Field Templates

Next we’re going to create some templates for our field. We’ll need both an editor template and a standard (non-admin) view template. We’ll start with the view template, because it is reasonably straightforward. In the Views directory, create a new directory named Fields and add to it a file named Contrib.Places.cshtml:

@using Orchard.Utility.Extensions

@{
    string name = Model.Name;

    string mapLink =
        "http://www.bing.com/maps/?v=2&cp=" +
        @Model.PlaceLatLong +
          "&lvl=12&dir=0&sty=r&where1=" +
        @Model.PlaceLatLong.ToString().Replace("~", ",");

    string iframeSource =
        "http://www.bing.com/maps/embed/?v=2&cp=" +
             @Model.PlaceLatLong +
               "&lvl=15&amp;dir=0&sty=r&where1=" +
            @Model.PlaceLatLong.ToString().Replace("~", ",") +
               "&form=LMLTEW&pp=" + @Model.PlaceLatLong +
               "&emid=30e5aeb2-f963-4089-e9b3-9bd5dfe07759";

}

<p class="text-field">
    @name.CamelFriendly():
    @if (!Model.ShowLink && ! Model.ShowMap) {
        <text>@Model.PlaceName</text>
    }
    @if (Model.ShowLink) {
        <a href="@mapLink" target="_blank">@Model.PlaceName</a>
    }
    @if (Model.ShowMap) {
        <text>@Model.PlaceName</text>
        <div id="mapviewer">
            <iframe id="map" scrolling="no"
               width="400" height="300" frameborder="0"
               src="@iframeSource">
            </iframe>
        </div>
    }
</p>

After importing the Orchard.Utilities.Extensions namespace, we define three variables to be used in the rendering of our field. Next, the display option chosen by the creator of the content type is interrogated to determine how we should display the place on our content item (e.g., an Event item). The logic here is fairly straightforward.

Note

The CamelFriendly extension method required declaring the string name variable since we can’t use extension methods with dynamic expressions such as Model.Name.

Next we’ll create the editor templates. In the Views directory, create a new directory named EditorTemplates with a subdirectory named Fields. Add to that directory a file named Contrib.Places.cshtml. This template will provide a simple UI for selecting a category, inputting a postal code and then selecting the place. We’ll skip validation for the sake of keeping the form from getting more complicated:

@using Orchard.Utility.Extensions
@model Contrib.PlacesField.ViewModels.PlacesFieldViewModel
@{
    Style.Require("jQueryUI_Orchard");

    Script.Require("jQuery");
    Script.Require("jQueryUtils");
    Script.Require("jQueryUI_Core");
    Script.Require("jQueryUI_Widget");
    Script.Require("jQueryUI_Autocomplete");
}
<fieldset>
    <label for="@Html.FieldIdFor(m => Model.PlaceName)">
        @Model.Name.CamelFriendly()</label>
    <p />
    <label class="forpicker"
        for="@Html.FieldIdFor(m => Model.Category)_CategoriesSelector">Category</label>
    <input type="text"
        id="@Html.FieldIdFor(m => Model.Category)_CategoriesSelector"
        class="text"/>
    @Html.HiddenFor(m => m.Category)

    <p />
    <label class="forpicker"
        for="@Html.FieldIdFor(m => Model.PostalCode)">Postal Code</label>
    @Html.EditorFor(m => m.PostalCode)

    <p />
    <label class="forpicker"
        for="@Html.FieldIdFor(m => Model.PlaceName)">Place</label>
    @Html.EditorFor(m => m.PlaceName)
    @Html.HiddenFor(m => m.PlaceLatLong)

</fieldset>


@using (Script.Foot())
{
<script type="text/javascript">
    function split(val) {
        return val.split(/,s*/);
    }
    function extractLast(term) {
        return split(term).pop();
    }

    $(function () {

        $("#@Html.FieldIdFor(m => Model.Category)_CategoriesSelector")
            .autocomplete({
                source: '@Url.Content("~/Admin/PlacesField/Yelp/Categories")',
                minLength: 2,
                select: function (event, ui) {
                    $("#@Html.FieldIdFor(m => Model.Category)")
                        .val(ui.item.value);
                    $("#@Html.FieldIdFor(m => Model.Category)_CategoriesSelector")
                        .val(ui.item.label);
                    return false;
                }
            });

        $("#@Html.FieldIdFor(m => Model.PlaceName)").focus(function () {

            $("#@Html.FieldIdFor(m => Model.PlaceName)").autocomplete({
                source: '@Url.Content("~/Admin/PlacesField/Yelp/Places")'
                    + '?categories='
                    + $("#@Html.FieldIdFor(m => Model.Category)").val()
                    + '&postalCode='
                    + $("#@Html.FieldIdFor(m => Model.PostalCode)").val(),
                minLength: 1,
                select: function (event, ui) {
                    $("#@Html.FieldIdFor(m => Model.PlaceLatLong)")
                        .val(ui.item.value);
                    $("#@Html.FieldIdFor(m => Model.PlaceName)")
                        .val(ui.item.label);
                    return false;
                }
            });

        });

    });
</script>
}

It might look like there’s a lot happening in this view, but it’s relatively straightforward. We’re using the jQuery UI Autocomplete plugin to render our short list of Yelp categories. The autocomplete code for categories and places is in the script block at the bottom of our Razor template.

How the Autocomplete plugin works isn’t important for understanding how to create a field, so we’ll skip a detailed explanation. The important thing to understand is that we’ve created a form with three fields for collecting information relevant to our places selection. We use hidden fields to save category and places data that is persisted, but not displayed to the user (category ID, latitude, and longitude).

For Orchard to know where to put our field, we’ll need to create a Placement.info file at the root of our module project. The entries in this file will allow our templates to appear in both admin forms and content displays. We saw how placement files work in Displaying Content. Without this file, your field will not display:

<Placement>
  <Place Fields_Contrib_Places_Edit="Content:2.5"/>
  <Place Fields_Contrib_Places="Content:2.5"/>
</Placement>

Settings

Next we’ll take care of persisting the settings. We’re going to need to hook into admin editor events in order to save our settings. This code is mostly boilerplate and you’ll find yourself copying, pasting, and modifying it as you create your own fields that require settings. Start by creating a new file named PlacesFieldEditorEvents.cs in the Settings directory that we created earlier.

In PartFieldEditor, we help Orchard load our settings form within the context of a content type editor form (we’ll see where this form is rendered before the end of the chapter). PartFieldEditorUpdate is used to handle the actual saving of our settings.

These two methods are similar to Editor and EditorTemplate in our field’s driver class:

using System.Collections.Generic;
using Contrib.PlacesField.Settings;
using Orchard.ContentManagement;
using Orchard.ContentManagement.MetaData;
using Orchard.ContentManagement.MetaData.Builders;
using Orchard.ContentManagement.MetaData.Models;
using Orchard.ContentManagement.ViewModels;

public class PlacesFieldEditorEvents : ContentDefinitionEditorEventsBase {

    public override IEnumerable<TemplateViewModel>
      PartFieldEditor(ContentPartFieldDefinition definition) {
          if (definition.FieldDefinition.Name == "PlacesField") {
               var model = definition.Settings.GetModel<PlacesFieldSettings>();
               yield return DefinitionTemplate(model);
          }
    }

    public override IEnumerable<TemplateViewModel> PartFieldEditorUpdate(
      ContentPartFieldDefinitionBuilder builder, IUpdateModel updateModel) {
          var model = new PlacesFieldSettings();
          if (builder.FieldType != "PlacesField") {
               yield break;
          }

          if (updateModel.TryUpdateModel(
            model, "PlacesFieldSettings", null, null)) {
               builder.WithSetting("PlacesFieldSettings.DisplayOptions",
                                            model.DisplayOptions.ToString());
          }

          yield return DefinitionTemplate(model);
    }
}

To create the editor template for our settings, create a new file named PlacesFieldSettings.cshtml in a new directory under Views named DefinitionTemplates. Note that this template will appear when creating content types, not content items. It’s the creator of the type, not the content item that will set this value. In our template, we’ll simply render an HTML select list from the values in our PlacesFieldDisplayOptions enumeration:

@model Contrib.PlacesField.Settings.PlacesFieldSettings
@using Contrib.PlacesField.Settings;

<fieldset>
    <label for="@Html.FieldIdFor(m => m.DisplayOptions)"
      class="forcheckbox">Display options</label>
    <select id="@Html.FieldIdFor(m => m.DisplayOptions)"
      name="@Html.FieldNameFor(m => m.DisplayOptions)">
        @Html.SelectOption(PlacesFieldDisplayOptions.NameOnly,
          Model.DisplayOptions == PlacesFieldDisplayOptions.NameOnly,
          "Name only")
        @Html.SelectOption(PlacesFieldDisplayOptions.NameAndLinkToMap,
          Model.DisplayOptions
            == PlacesFieldDisplayOptions.NameAndLinkToMap,
          "Name and Link to Map")
        @Html.SelectOption(PlacesFieldDisplayOptions.NameAndEmbeddedMap,
          Model.DisplayOptions
            == PlacesFieldDisplayOptions.NameAndEmbeddedMap,
          "Name and Embedded Map")
    </select>

    @Html.ValidationMessageFor(m => m.DisplayOptions)

</fieldset>

Controllers

One final important detail is the data. So far, we’ve created a pretty complex field, but it won’t actually do anything until we provide it a way to get data from the Yelp API. As I previously mentioned, the API is a simple REST API. However, we can’t query it directly from our views, because browser security restrictions won’t allow AJAX requests to other servers.

To solve this problem, we’ll have to create a solution that runs on our Orchard site. Specifically, we’re going to create an ASP.NET MVC controller class that will handle the AJAX requests that are generated by the jQuery Autocomplete plugin. If you revisit the code for our editor view, you’ll see the URLs that we’re going to call.

URL to retrieve Yelp Categories

/Admin/PlacesField/Yelp/Categories

URL to retrieve Yelp Places

/Admin/PlacesField/Yelp/Places

If you’re familiar with MVC, you probably recognize that we’re going to create a controller named YelpController with two action methods, Categories and Places. We’re going to examine the controller shortly, but for now we need to setup a route to tell Orchard and MVC how to map our requests. Orchard will automatically map all routes by finding classes that implement IRouteProvider. Save this file as Routes.cs at the root of the module project:

using System.Collections.Generic;
using System.Web.Mvc;
using System.Web.Routing;
using Orchard.Mvc.Routes;

public class Routes : IRouteProvider {
    public void GetRoutes(ICollection<RouteDescriptor> routes) {
          foreach (var routeDescriptor in GetRoutes())
               routes.Add(routeDescriptor);
    }

    public IEnumerable<RouteDescriptor> GetRoutes() {
          return new[] {
               new RouteDescriptor {
                     Priority = 5,
                     Route = new Route(
                          "Admin/PlacesField/{controller}/{action}",
                          new RouteValueDictionary {
                                {"area", "Contrib.PlacesField"},
                                {"controller", "Yelp"},
                                {"action", "Places"}
                          },
                          new RouteValueDictionary(),
                          new RouteValueDictionary {
                                {"area", "Contrib.PlacesField"}
                          },
                          new MvcRouteHandler())
               }
          };
    }
}

Note

The area defined in the RouteValueDictionary must be the name of the module and not an area that is defined as a URL part as is expected with typical MVC routing.

The Controller class is a standard MVC controller. Save this file as YelpController.cs in the Controllers directory that the code generation tools created for the module project:

using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using YelpSharp;
using YelpSharp.Data.Options;

public class YelpController : Controller {

    private readonly Yelp _client;

    public YelpController() {
          var options = new Options()
          {
               AccessToken = "<YOUR_ACCESS_TOKEN>",
               AccessTokenSecret = "<YOUR_ACCESS_TOKEN_SECRET>",
               ConsumerKey = "<YOUR_ACCESS_CONSUMER_KEY>",
               ConsumerSecret = "<YOUR_CONSUMER_SECRET>"
          };

          _client = new Yelp(options);
    }

    public YelpController(Yelp client) {
          _client = client;
    }

    public ActionResult Places(string postalCode,
                          string category, string term) {

          var so = new SearchOptions() {
               LocationOptions = new LocationOptions()      {
                     location = postalCode
               },
               GeneralOptions = new GeneralOptions(){
                     category_filter = category,
                     term = term
               }
          };
          var results = _client.Search(so);

          return Json(results.businesses.Select(
               p => new { value = p.location.coordinate.latitude + "~"
                                  + p. location.coordinate.longitude,
                           label = p.name }).ToArray()
               , JsonRequestBehavior.AllowGet);
    }

    public ActionResult Categories(string postalCode,
                          string categories, string term) {

          var categoryList = new List<string> {
               "Restaurants", "Nightlife", "Arts"
          };

          var results = string.IsNullOrEmpty(term) ?
               categoryList : categoryList
                     .Where(c =>
                          c.ToLower()
                          .Contains(term.ToLower()));
          return Json(results.Select(
               c => new { value = c.ToLower(), label = c })
               .ToArray(), JsonRequestBehavior.AllowGet);
    }
}

Most of the logic in the controller deals with retrieving data from the Yelp service. For this code to work, you’ll need to add the YelpSharp Nuget package to your field project:

PM> Install-Package YelpSharp

Note

You’ll need to get free API credentials from http://www.yelp.com/developers/getting_started. These API credentials then need to be entered into the constructor of the controller, where the Yelp client is instantiated. If you forget this step, your field won’t work. Request the 2.0 API on the API access page. It will give you all four keys that you’ll require.

Again, this class is a standard MVC controller. There isn’t anything Orchard-specific to consider here. Basically, all that’s happening in each action (public method that returns an ActionResult) is that we’re querying the Yelp API method and returning a JSON serialized result of the Yelp data.

The YelpSharp client takes care of deserializing the Yelp response to POCO classes that we’ll then serialize to JSON formats that our autocomplete textboxes will consume. We use a LINQ projection to create a JSON structure that is friendly to the Autocomplete plugin.

Module Metadata

The final step is to set the metadata for our field in Module.txt. These settings are straightforward and include author and module description. If we had multiple fields in our project we could list multiple features, which could be enabled or disabled separately. Finally, we’ll also declare a set of modules upon which our field is dependent:

Name: Places Field
AntiForgery: enabled
Author: John Zablocki
Website: http://dllhell.net
Version: 1.0
OrchardVersion: 1.3
Description: The Places Field allows for location lookups
Features:
    Contrib.PlacesField:
        Name: Places Field
        Description: Places fields.
        Category: Fields
        Dependencies: Orchard.jQuery, Common, Settings

Using the Places Field

Now that we’ve created our field, it’s time to compile the project and enable the module. Build your project to make sure you’ve successfully added all the using directives and Nuget references that our code demands. Then return to your Orchard command-line session and enable the new module:

PM> feature enable Contrib.PlacesField
Enabling features Contrib.PlacesField
Places Field was enabled

Creating Content with the Places Field

Now that our field is coded and enabled, it’s time to test it out. Return to the Dashboard and select ContentContent Types and select “Edit” on the listing for “Event.” Click “Remove” on the field listing for the existing “Location” field. Then click “Add Field” and select the field type “Places Field.” Name the field “Event Location,” accepting the default technical name as usual. Click “Save” and the “Event Location” field will appear in the list of fields.

Note

We could add several PlacesField elements to our Event content type if we wanted to, which is why we had to prefix our autocompleter client side IDs with field specific IDs. In other words, when you select a place for PlacesFieldA it won’t get mixed up with PlacesFieldB.

Click “>” next to the field name to be presented with our settings editor. It’s here while defining our content type that we’ll set the display option for this field, affecting all items created from this content type definition. From the select list with our display options (Figure 5-1), choose “Name and Embedded Map” and click Save.

The Places Field settings template

Figure 5-1. The Places Field settings template

If you were to browse to the “Events” listing page on our site, you’d see a Bing Maps error because we haven’t selected a place and we set our field to render a map. You should also see a Bing Maps error on your content item listing (ContentContent Items), because Orchard uses the same field display template to show a preview of your content item in the admin content listing page. That template includes the map code.

To fix these errors, we’ll simply select a place. Click ContentContent Items and then click “Edit” on the event listing you wish to edit. As you start typing in the “Category” field, you should see a list of categories that match your input. After selecting a category and entering a zip code, go ahead and search for a place by typing in the “Place” textbox.

Note

Yelp provides a full list of available categories that can be used to filter out results at http://www.yelp.com/developers/documentation/category_list. This module limits searches to only those categories that seem likely to host live music. A more robust solution would also provide some better caching and filtering of results.

The Places Field editor template

Figure 5-2. The Places Field editor template

After you’ve selected a place, click “Publish Now” and then return to the site and refresh the “Events” page. You should now see the event location displayed with both the name of the place you selected and an embedded Bing map (Figure 5-3).

The Places Field displayed with a Bing map

Figure 5-3. The Places Field displayed with a Bing map

Displaying the Places Field

If you click over to the home page (or any page that isn’t the “Events” page) you’ll see that our event location map is overtaking the space it’s been allotted in the AsideSecond zone. Our “Upcoming Events” HTML widget is trying to use the embedded map in a smaller space than our event listing or event detail pages.

In Chapter 3, we learned how to customize the display of fields and other types of content. Our own fields may be customized in the same way. Copy Contrib.Places.cshtml from the Fields directory under the Views directory in our module. Paste it in the Views directory of our “DaisysTheme” theme and rename it Fields.Contrib.Places.cshtml. Change the height and width of the iframe that includes the map and refresh the “Events” page. You’ll see that the embedded map has changed to reflect your chosen size:

<iframe id="map" scrolling="no" width="300" height="200"
          frameborder="0" src="@iframeSource">

We also could choose to remove the entire Contrib.Places field from any summary listing by updating the Placement.info file for our template (Figure 5-4):

<Match DisplayType="Summary">
    <Place Fields_Contrib_Places="-" />
</Match>
Using placement to remove a field

Figure 5-4. Using placement to remove a field

Summary

Though we took a few shortcuts, our PlacesField is a reusable Orchard component. We could easily share it in other projects simply by copying the project to another Orchard solution. A better solution, which we’ll learn about in a later chapter, would be to package it up using the Orchard tools and submit it to the Orchard gallery (or some private file share).

Content fields are just one type of module we can create in Orchard. In the next chapter, we’re going to create a more complex (in terms of behavior) module, namely a content part that we’ll turn into a widget. Much of what we learned during our field exercise will still apply, but there will be several new concepts to learn.

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

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