Create a Book widget
The following tutorial demonstrates how to create a simple MVC Books widget. You leverage the attribute routing of MVC 5 in the context of a Sitefinity CMS widget. The Books widget displays a list of books on the frontend and tracks on the client the points retrieved for each book.
Install the Books widget
- Clone the MVC samples repository.
- Build the
BooksWidgetproject, located in the AttributeRouting sample. - Reference the
BookWidget.dllfrom your Sitefinity web application.
Create the Books widget
Sitefinity CMS makes it possible to have MVC widgets that are stored in separate assemblies. The following sample creates the Books widget in a separate assembly.
Perform the following:
- Create a new class library named
BooksWidget. - In Visual Studio, in the Package Manager Console, make sure
BooksWidgetproject is selected as default project. Install the required packages:Telerik.Sitefinity.CoreTelerik.Sitefinity.MvcTelerik.Sitefinity.Feather
- Modify the
AssemblyInfo.csby adding the following code:Textusing Telerik.Sitefinity.Frontend.Mvc.Infrastructure.Controllers.Attributes; [assembly: ControllerContainer] - Create the following folders:
MVCMVC\ViewsMVC\Views\BooksMVC\ControllersMVC\Scripts
Create the model classes
In the MVC/Models folder, create a new class named Book. The class needs to have:
AuthorpropertyTitlepropertyPointspropertyVotemethod to increment the points
The Book class should look similar to the following:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace BooksWidget.Mvc.Models
{
public sealed class Book
{
public Book(string author, string title)
{
this._author = author;
this._title = title;
this._points = 0;
}
public string Author
{
get
{
return this._author;
}
}
public string Title
{
get
{
return this._title;
}
}
public int Points
{
get
{
return this._points;
}
}
public void Vote()
{
this._points++;
}
private readonly string _title;
private readonly string _author;
private volatile int _points;
}
}
Next, in the MVC/Models folder, you create a new class named BooksViewModel. The class needs to have the following properties:
PageCountCurrentPageNextPageUrlPreviousPageUrl
The BooksViewModel class should look similar to the following:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using Telerik.Sitefinity.Services;
namespace BooksWidget.Mvc.Models
{
public sealed class BooksViewModel
{
public BooksViewModel(IEnumerable<Book> items, int pageCount, int currentPage)
{
this._items = items;
this._pageCount = pageCount;
this._currentPage = currentPage;
}
public IEnumerable<Book> Items
{
get
{
return this._items;
}
}
public int PageCount
{
get
{
return this._pageCount;
}
}
public int CurrentPage
{
get
{
return this._currentPage;
}
}
public string NextPageUrl
{
get
{
return this.IndexActionUrl(this.CurrentPage + 1);
}
}
public string PreviousPageUrl
{
get
{
return this.IndexActionUrl(this.CurrentPage - 1);
}
}
private string IndexActionUrl(int page)
{
StringBuilder sb = new StringBuilder();
var currentNode = SiteMap.CurrentNode;
if (currentNode != null)
sb.Append(currentNode.Url);
else
sb.Append(VirtualPathUtility.RemoveTrailingSlash(SystemManager.CurrentHttpContext.Request.Path));
sb.Append("/");
sb.Append(page);
return sb.ToString();
}
private readonly IEnumerable<Book> _items;
private readonly int _pageCount;
private readonly int _currentPage;
}
}
Create the controller
Perform the following:
- In folder
MVC/Controllers, create a new class that derives from theSystem.Web.Mvc.Controllerclass and name itBooksController``. - Add the fields to the
BooksControllerclass. - Create an
Indexaction with route relative to the current page. The action accepts the page number as an argument. - Create
PointsandVoteactions with a direct route, so that they can be accessed with AJAX calls. - Add a
ControllerToolboxItemattribute to theBooksControllerclass.
Thus, Sitefinity CMS automatically adds the Books widget in the toolbox.
Use the following code sample:
using BooksWidget.Mvc.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Telerik.Sitefinity.Mvc;
namespace BooksWidget.Mvc.Controllers
{
[ControllerToolboxItem(Name = "Books", SectionName = "Feather samples", Title = "Books")]
public class BooksController : Controller
{
[RelativeRoute("{page:int:min(1)?}")]
public ActionResult Index(int? page)
{
IEnumerable<Book> items = BooksController._library;
if (page.HasValue)
items = items.Skip((page.Value - 1) * BooksController.PageSize);
items = items.Take(BooksController.PageSize);
var viewModel = new BooksViewModel(items, (int)Math.Ceiling(BooksController._library.Count / (double)BooksController.PageSize), page ?? 1);
return this.View(viewModel);
}
[Route("web-interface/books/points/{page:int:min(1)?}")]
public JsonResult Points(int? page)
{
IEnumerable<int> points = BooksController._library.Select(book => book.Points);
if (page.HasValue)
points = points.Skip((page.Value - 1) * BooksController.PageSize);
points = points.Take(BooksController.PageSize);
return this.Json(points, JsonRequestBehavior.AllowGet);
}
[HttpPost, Route("web-interface/books/vote/{id:int:min(0):max(9)}")]
public JsonResult Vote(int id)
{
var book = BooksController._library[id];
book.Vote();
return this.Json(book.Points, JsonRequestBehavior.DenyGet);
}
private const int PageSize = 5;
private static readonly List<Book> _library = new List<Book>(10)
{
new Book("Beatrix Potter", "The Tale Of Peter Rabbit"),
new Book("Julia Donaldson", "The Gruffalo"),
new Book("Michael Rosen", "We're Going on a Bear Hunt"),
new Book("Judith Kerr", "The Tiger Who Came to Tea"),
new Book("AA Milne", "Winnie the Pooh"),
new Book("Enid Blyton", "The Enchanted Wood"),
new Book("Jill Murphy", "The Worst Witch"),
new Book("Roald Dahl", "Charlie and the Chocolate Factory"),
new Book("Jacqueline Wilson", "The Story of Tracy Beaker"),
new Book("Michelle Magorian", "Goodnight Mister Tom")
};
}
}
Create the view
You need to create an Index view, because this is the only view that is used by the BooksController. To do this, you must create a new Razor view named Index and to place it in the MVC/Views/Books folder.
To create the Index view, use the following code:
@model BooksWidget.Mvc.Models.BooksViewModel
@using Telerik.Sitefinity.Modules.Pages;
@using Telerik.Sitefinity.Frontend.Mvc.Helpers;
@Html.Script(ScriptRef.JQuery, "top")
<div data-role="books-widget">
@foreach (var book in Model.Items)
{
<div>
<p>@book.Author, @book.Title</p>
<p>Points: <span data-role="points">@book.Points</span> <a href="" data-role="vote-link">[Vote]</a></p>
</div>
}
@if (Model.CurrentPage > 1)
{
<a href='@Url.Content(Model.PreviousPageUrl)'>[Previous]</a>
}
@Model.CurrentPage / @Model.PageCount
@if (Model.CurrentPage < Model.PageCount)
{
<a href='@Url.Content(Model.NextPageUrl)'>[Next]</a>
}
<input data-role="current-page" type="hidden" value="@Model.CurrentPage" />
</div>
@Html.Script(Url.WidgetContent("Mvc/Scripts/books-widget.js"), "bottom")
NOTE: You can create a Razor view in a class library project by selecting HTML Page from the Add New Item dialog, and then renaming the file extension to
.cshtml. In the file properties, set the view as Embedded Resource.
Create the client-side script
In the MVC/Scripts folder, create a JavaScript file named books-widget.js. This script will retrieve and update the books' points calling the JSON actions:
; (function ($) {
var initializeBooksWidget = function (element) { var sf_appPath = window.sf_appPath || "/";
var widget = $(element);
var currentPage = widget.find('input[data-role=current-page]').val();
var pointSpans = widget.find('span[data-role=points]');
$.get(sf_appPath + 'web-interface/books/points/' + currentPage, function (data) {
for (var i = 0; i < pointSpans.length && i < data.length; i++) {
$(pointSpans[i]).html(data[i]);
}
});
widget.find('a[data-role=vote-link]').each(function (index, value) {
var link = $(value);
var id = index + (currentPage - 1) * 5; // PageSize is a constant 5
var idx = index;
link.click(function () {
$.post(sf_appPath + 'web-interface/books/vote/' + id, function (data) {
$(pointSpans[idx]).html(data);
});
return false;
});
});
};
$(function () {
$('div[data-role=books-widget]').each(function (index, value) {
initializeBooksWidget(value);
});
});
})(jQuery);
NOTE: Be sure to mark the script as an Embedded Resource in the file properties.
You can now build the project and test the result by placing the Books widget on a page.