Custom Ad system: OrchadCMS+WebApi+MvvmCross Part I [Server Side]

May 22, 2013 jmgomez | ASP.NET MVC , OrchardCMS
Custom Ad system: OrchadCMS+WebApi+MvvmCross Part I [Server Side]

Introduction:

 

One of my customers wanted a very simple ad system. He wants to be able to publish ads via web, so they'll be reflected in his iOS native app built on top of Xamarin.iOS and MvvmCross. In this series of two posts, I'll be talking about how to expose custom Orchard's manager content via WebApi and take it from a client built with MvvmCross. So the first part, this post, is about the server side and soon I'll publish the client side.

 

Scenario:

 

Actually this is a very simple scenario, I also tried to simplify it more in order to keep the post as short as possible. So we are only going to show one active ad, and it'll contain: Image or banner, a title, and a destiny url that will be the direction of the website that will be triggered when a user touches the banner, and finally a boolean field that indicates if the ad is active or not.

 

As we want to be as productive as possible, we are going to take advantage of Orchard's content manager system and we'll create a Content Type, called Ad and we are going to add to it the following parts:

 

->CommonPart: it's necessary because Orchard uses it for many things, like to list the ad content types that we'll create in the dashboard, otherwise the content doesn't appear.

 

->TitlePart: It will be the field that will represent the title of the ad. We can create our field, but we want the title to appear when Orchard shows our ContentType. That's what we need it for.

 

->AdPart: This will be our custom content part. And it will contain:

                ->A boolean field, which indicates if the ad is Active or not.

                ->A string field, which represents the destiny url.

                ->A ContentField of type Contrib.ImageField, thanks to this module we don't have to worry about all the image stuff. We're going to get the path of the image from this module.

 

As we don't want to expose the content in the front end, this is enough for our purpose.

 

 

Making the module:

 

I personally like to use the codegen tool to create modules. So make sure that you have codegen activated and go to the command line and write:

 

codegen module jmgomez.CustomAds /IncludeInSolution:true

 

codegen datamigration jmgomez.CustomAds

 

codegen controller jmgomez.CustomAds AdsController

 

This will create our project and will add a migration and a controller.

 

Now we can create our Record and our Part, we're going to make a folder called Model and we are going to add two classes:

publicclassAdRecord : ContentPartRecord {

        publicvirtualbool Active { get; set; }

        publicvirtualstring Url { get; set; } 

    }

 

publicclassAdPart : ContentPart<AdRecord> {

       

        publicbool Active {

            get { returnthis.Record.Active; }

            set { this.Record.Active = value; }

        }

      

        [Required]

        publicstring Url {

            get { returnthis.Record.Url; }

            set { this.Record.Url = value; }

        }

Now we can complete our Migration:

publicclassMigrations : DataMigrationImpl {

        publicint Create() {

            SchemaBuilder.CreateTable("AdRecord", (table) => table

                .ContentPartRecord()

                .Column<string>("Url")

                .Column<bool>("Active"));

           

            ContentDefinitionManager.AlterPartDefinition("AdPart",(part)=>part

                .WithField("Image",cfg=>cfg

                    .OfType("ImageField")

                    .WithDisplayName("Image for ad")

                    .WithSetting("ImageFieldSettings.Required","true")

                    ));

 

           ContentDefinitionManager.AlterTypeDefinition("Ad",(type)=>type

               .Creatable()

               .WithPart("AdPart")

               .WithPart("CommonPart")

               .WithPart("TitlePart")

               );

 

            return 1;

        }

      

 

    }

We defined the requisites of our scenario. We just created the Content Part and the Content Type that we need.

 

Now we can do the View, in the directory  Views/EditorTemplates/Parts/Ad.cshtml and we'll add the following code:

@model jmgomez.CustomAds.Models.AdPart

<fieldset>

  <legend>Ad Fields</legend>

 

<divclass="editor-label">

    @Html.LabelFor(model => model.Url)

</div>

<divclass="editor-field">

    @Html.TextBoxFor(model => model.Url)

    @Html.ValidationMessageFor(model => model.Url)

</div>  

 

<divclass="editor-label">

   @Html.LabelFor(model=>model.Active)

</div>   

<divclass="editor-field">

    @Html.CheckBoxFor(model=>model.Active)

    @T("If you activate this box, this will be the current ad")       

</div>

 

</fieldset>

In order to show the View, we need a Driver, so we just create it in the Driver folder.

 

publicclassAdDriver : ContentPartDriver<AdPart> {

        privatereadonlyIAdsService _adsService;

        //Get

        public AdDriver(IAdsService adsService) {

            _adsService = adsService;

        }

 

        protectedoverrideDriverResult Editor(AdPart part, dynamic shapeHelper) {

 

            return ContentShape("Parts_Ad_Edit",

                () => shapeHelper.EditorTemplate(

                    TemplateName: "Parts/Ad",

                    Model: part,

                    Prefix: Prefix));

        }

        //Post

        protectedoverrideDriverResult Editor(AdPart part, IUpdateModel updater, dynamic shapeHelper) {

            updater.TryUpdateModel(part, Prefix, null, null);

            if (part.Active)

                _adsService.DeactivateAds(part.Id);

            return Editor(part, shapeHelper);

        }

    }

We don't have a Display method because we don't need it. Note that we are injecting the IAdsService dependency via constructor, which contains the logic of our part. If the ad is activated when we are going to update our ContentPart then we also have to deactivate the others (we only want to show one ad at the same time).

 

 

We need to create a Service folder and add to it IAdsService and AdsService:

 

publicinterfaceIAdsService : IDependency {

        void DeactivateAds(int idAd);

        AdViewModel GetActiveAd();

    }

publicclassAdService : IAdsService {

        privatereadonlyIContentManager _contentManager;

      

 

        public AdService(IContentManager contentManager) {

            _contentManager = contentManager;

        }

 

        publicvoid DeactivateAds(int idAd) {

            var activatedAds = _contentManager.List<AdPart>("Ad")

                .Where(ad => ad.Active && ad.Id!=idAd);

            foreach (var ad in activatedAds) {

                ad.Active = false;

            }

        }

 

        publicAdViewModel GetActiveAd() {

            var adPart = _contentManager.List<AdPart>("Ad").SingleOrDefault(ad => ad.Active);

            var imageField = adPart.Fields.OfType<ImageField>().First();

            var imagePath = VirtualPathUtility.ToAbsolute(imageField.FileName);

            var titlePart = adPart.As<TitlePart>();

           

           

            returnnewAdViewModel()

            {

                Title = titlePart.Title,

                Url = adPart.Url,

                ImagePath = imagePath

            };

        }

    }

 

We only have two methods, DeactivateAds that deactivates all the ads (Disclaimer: again it's only for educational purpose, it's better to keep the active ad in a var and just change that var) and we also have a method that returns the active ad. We are going to make a AdViewModel class in a new ViewModel folder:

 

  publicclassAdViewModel {

        publicstring Title { get; set; }

        publicstring Url { get; set; }

        publicstring ImagePath { get; set; }

    }

And finally this is the controller that exposes our data:

 

      publicclassAdsController : ApiController {

        privatereadonlyIAdsService _adsService;

 

        public AdsController(IAdsService adsService) {

            _adsService = adsService;

        

        }

 

        publicAdViewModel GetAd() {

            return _adsService.GetActiveAd();

        }

 

     

    }

Pretty easy, isn't?

 

Now we need to create a filter in order to save the content in the database, just add a Handler folder and this class inside it:

publicclassAdHandler : ContentHandler {

 

        public AdHandler(IRepository<AdRecord> repository ) {

            Filters.Add(StorageFilter.For(repository));

        }

    }

We also want that our users can see all the ads that they will create, so with Orchard this is only a few lines of code:

publicclassAdminMenu : INavigationProvider {

        publicLocalizer T { get; set; }

 

        publicstring MenuName

        {

            get { return"admin"; }

        }

 

        publicvoid GetNavigation(NavigationBuilder builder)

        {

            builder.Add(T("Ad"), "1", BuildMenu);

        }

 

        privatevoid BuildMenu(NavigationItemBuilder menu)

        {

            menu.Add(T("List"), "1.0", item =>

                item.Action("List", "Admin", new { area = "Contents", id = "Ad" }));

 

            menu.Add(T("New Ad"), "1.1", item =>

                item.Action("Create", "Admin", new { area = "Contents", id = "Ad" }));

 

        }

    }

Pretty sweet.

 

We also want the route to look nice, so we create a custom route:

publicclassHttpRoutes : IHttpRouteProvider {

        publicIEnumerable<RouteDescriptor> GetRoutes() {

            returnnew[] {

                newHttpRouteDescriptor() {

                    Name = "Ad",

                    Priority = -10,

                    RouteTemplate = "Ad",

                    Defaults = new {

                        area = "jmgomez.CustomAds",

                        controller = "Ads"

 

                    }

                }

 

            };

        }

 

        publicvoid GetRoutes(ICollection<RouteDescriptor> routes) {

            foreach (var routeDescriptor inthis.GetRoutes())

                routes.Add(routeDescriptor);   

           

        }

    }

And finally, we need the metadata of the module and also the placement.info:

 

Name: jmgomez.CustomAds

AntiForgery: enabled

Author: jmgomez

Website:jmgomez.me

Version: 1.0

OrchardVersion: 1.0

Description: Description for the module

Dependencies: Contrib.ImageField

Features:

    jmgomez.CustomAds:

        Description: Description for feature jmgomez.CustomsAds.

 

Placement:

<Placement>

  <PlaceParts_Ad_Edit="Content:1"></Place>

  <PlaceFields_Contrib_Image_Edit="Content:2"/>

</Placement>

When we create a first ad and we navigate with our browser to the route host/ad we have this:

 

<AdViewModel xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/jmgomez.CustomAds.ViewModel">

<ImagePath>/Media/Default/Ad/Image/portadaweb-640x290-1.jpg</ImagePath>

<Title>Prueba ad</Title>

<Url>www.google.es</Url>

</AdViewModel>

 

It's XML because of the header of the browser (chrome in my case). When we'll make the request in the mobile device, we are going to specify Json.

 

Conclusion:

We exposed Orchard's manager content, if we did that with a common ASP.NET MVC that would be a lot more of code, time and money. The sample is very simple but it shows the power of Orchard. In the next post I'm going to show you the power of MvvmCross.

 

The code is here

 

Sorry for the format of the code, I promise that I'm going to fix it soon :)