BForms Grid and Toolbar components are designed to provide rich functionality, cross-browser, cross-device and internationalization support for tabular data providing full CRUD and search operations over complex datasets.

The feature set includes AJAX-enabled paging, sorting, editing, filtering and built-in data repository adapted for Entity Framework that can be customized with your own query logic. The UI is touch friendly and HTML5 enhanced featuring edit in place, master-details view and multi row select with bulk actions.

On the client side, the Grid is deployed as an AMD jQuery widget that supports theming and templates. The styling is done using bootstrap v3 CSS grid system.

Building a Grid

In this tutorial, you will learn how to implement the BForms.Grid from scratch. We start by presenting the models we’ll use. Then will begin building the grid to present our data, step by step, adding more functionality as we go. At this step you will have a grid that displays your data, with sorting and pagination capabilities. Next step is to add editing capabilities to our rows by enabling the details for each row. We’ll also implement the Bulk Actions that will help us modify multiple rows at a time.

Initial Project

We will start from an empty MVC Project. The project will be setup to use RequireJS and BForms. To see a guide on how to setup your initial project please follow this link: Setup BForms for ASP.NET MVC

We will work with mocked data that will fake a movies database.

Movie Model Class

public class Movie
{
    public int Id { get; set; }
    public string Title { get; set; }
    public decimal WeekendRevenue { get; set; }
    public decimal GrossRevenue { get; set; }
    public DateTime ReleaseDate { get; set; }
    public string Genres { get; set; } 
    public double Rating { get; set; }
    public bool IsRecommended { get; set; }
    public string Poster { get; set; }
}

            

Our mocked data will consist in a list of movie objects added in the BFormsContext Constructor method. Here is an example of a movie record:

new Movie()
{
    Id = 1,
    Title = "Jackass Presents: Bad Grandpa",
    WeekendRevenue = 32.1m,
    GrossRevenue = 32.1m,
    ReleaseDate = new DateTime(2013, 10, 25),
    Genres = string.Join(",", new List<MovieGenre>() {MovieGenre.Comedy}.ToArray()),
    Rating = 7.0,
    IsRecommended = false,
    Poster ="http://ia.media-imdb.com/images/M/MV5BMT.jpg"
}


            

The BFormsContext Object will be instantiated in the BaseController class. All our controllers will inherit from BaseController. This way we will have access to the BFormsContext from inside our controllers.

1. Creating the Models

In this step we will create the models needed to build the grid. In our project let’s create a folder named ‘Models’. Add a new empty class file to this folder named ‘MoviesModels.cs’. This file will contain all our grid related models.

The first model will be the row model. This model is used by the grid widget to populate the columns and the header. The fields can be annotated with the BsGridColumnAttribute. This attribute can set some properties for the columns. For example, the ‘Title’ column width is set to 4, is made sortable and editable.

public class MoviesRowModel
{
    public int Id { get; set; }

    [BsGridColumn(Width = 4, IsEditable = true, IsSortable = true)]
    public string Title { get; set; }

    [BsGridColumn(Width = 2, IsEditable = false, IsSortable = true)]
    public decimal WeekendRevenue { get; set; }

    [BsGridColumn(Width = 2, IsEditable = false, IsSortable = false)]
    public decimal GrossRevenue { get; set; }

    [BsGridColumn(Width = 2, IsEditable = false, IsSortable = true)]
    public DateTime ReleaseDate { get; set; }

    [BsGridColumn(Width = 2, IsEditable = false, IsSortable = true)]
    public bool Recommended { get; set; }

}

            

Here is how our grid will look for this row model:


The width of each column corresponds to the width specified in the model. The total width must be 12. Also because we have set the IsSortable property to false for the GrossRevenue column, you can see that the sorting is disabled in the grid, while for the other columns is enabled.


All this properties can be overridden in the view, but we’ll see this at a later time.

However this is not enough. We still need a View Model. The grid can’t use the Row Model directly. After you define the row model you have to wrap it with the BSGridModel and append it to the view model.

Here is how our Movies View Model will look like:

public class MoviesViewModel
{
    [BsGrid(HasDetails = false, Theme = BsTheme.Blue)]
    [Display(Name = "Top Movies")]
    public BsGridModel<MoviesRowModel> Grid { get; set; }
}


            

Using Data Annotation we can set the properties for the grid. We use Display(Name) to set the name shown above the grid header. You can see the text ‘Top Movies’ next to the number representing the total rows count.


We also set the BsGrid(HasDetails) to false. This means that the row will not expand to show more details. We’ll set this to true at a later time.

If we’ll add the BForms.Toolbar widget, our MoviesViewModel will also contain the Toolbar model. We’ll add it at a later time. For now we only need the Grid Model.

2. Creating the Grid Repository

In this step we will create the Movies Grid Repository. This repository will inherit from BsBaseGridRepository and will have to override a few methods. These methods will be used to filter and sort the grid.

In our project let’s create a folder named ‘Repositories’. Add a new empty class file to this folder named ‘MoviesGridRepository.cs’.

This is how the initial class will look:

public class MoviesGridRepository : BsBaseGridRepository<Movie, MoviesRowModel>
    {
    private BFormsContext db;

    public MoviesGridRepository(BFormsContext _db)
    {
        db = _db;
    }
}

            

We inherit from BsBaseGridRepository and we pass the Movie Entity and the MoviesRowModel. We also initialize the BFormsContext that we will later use to query the data set.

Next we have to implement the Query() method.

public override IQueryable<Movie> Query()
{
    var query = db.Movies.AsQueryable();

    return query;
}

            

This method is used to get the basic query. The items returned by this query will represent the total count, and will be ordered and mapped with the help of the methods we’ll implement next.

The second method that we have to implement is OrderQuery().

public override IOrderedQueryable<Movie> OrderQuery(IQueryable<Movie> query)
{
    this.orderedQueryBuilder.OrderFor(x => x.Recommended, y => y.IsRecommended);

    var ordered = this.orderedQueryBuilder.Order(query, x => x.OrderByDescending(y => y.WeekendRevenue));

    return ordered;
}

            

Here we use orderedQueryBuilder property to order the query. In this example we order the query by WeekendRevenue descending. You can use any property of your model to set the initial order for your grid. Also if you have columns that don’t have an exact correspondent in your entity model, you have to use the OrderFor() method to specify the mapping. In this example we specify that the Recommended property of our row model will correspond to the IsRecommended property of our Movie Entity. Another usage of the OrderFor method would be to map one column to multiple properties of the entity.

The final method we must implement is MapQuery(). This method is used to make the select and convert your entities in row models. To do this, you will most likely implement a mapper(TEntity, TRow) to use in the select.

Here is how the MapQuery() method will look like in our case:

public override IEnumerable<MoviesRowModel> MapQuery(IQueryable<Movie> query)
{
    return query.Select(MapMovie_MovieRowModel);
}

            

We also have to implement the MapMovie_MovieRowModel() used in the above example. This is how the mapper will look in our case:

public Func<Movie, MoviesRowModel> MapMovie_MovieRowModel = x =>
new MoviesRowModel
{
           Id = x.Id,
           Title = x.Title,
           WeekendRevenue = x.WeekendRevenue,
           GrossRevenue = x.GrossRevenue,
           ReleaseDate = x.ReleaseDate,
           Recommended = x.IsRecommended
        };


            

This is a delegate that will take a Movie Entity and will return a MovieRowModel.

At a later time we will enhance these methods to support search and filtering functionality.

3. Creating the Controller

In this step we will create the Grid Controller. This controller will contain the actions needed to display the grid. We will display our grid on the home page so our controller will be named 'HomeController'.

Create a new controller in the ‘Controllers’ folder. This controller will inherit from the BaseController. This is how it will look like:

public class HomeController : BaseController
{
    private readonly MoviesGridRepository _gridRepository;

    public HomeController()
    {
        _gridRepository = new MoviesGridRepository(Db);
    }
    public ActionResult Index()
    {
        return View();
    }
}

            

The Index() action of this controller will render the view that will contain our grid. However we have to do some changes before we can send the model the way our view will be expecting. We have to initialize the Grid Model with some initial settings like ‘Page’ and ‘PageSize’. Then we’ll use this model to initialize our View Model. The View Model will be send to the view.

Also we have to use RequireJsOptions.Add() method to send some data to the page. The data we need to send is the name of the ajax action that will be used by the grid pager to return other pages. This will be in the form of a Dictionary object that will contain for the moment only the pageUrl property.

This is how our Index Action will look like at the end:

public ActionResult Index()
{
    var gridModel = _gridRepository.ToBsGridViewModel(new BsGridBaseRepositorySettings());

    var model = new MoviesViewModel
    {
        Grid = gridModel,
    };

    var options = new Dictionary<string, string>
    {
        {"pagerUrl",  Url.Action("Pager")}
    };

    RequireJsOptions.Add("index", options);

    return View(model);
}

            

Next we have to implement our Pager() Action that will be called by ajax from our grid and will return the requested page as BsJsonResult. Here is the final implementation of the method:

public BsJsonResult Pager(BsGridBaseRepositorySettings settings)
{
    var msg = string.Empty;
    var status = BsResponseStatus.Success;
    var html = string.Empty;
    var count = 0;

    try
    {
        var viewModel = _gridRepository.ToBsGridViewModel(settings, out count).Wrap<MoviesViewModel>(x => x.Grid);

        html = this.BsRenderPartialView("Grid/_Grid", viewModel);
    }
    catch (Exception ex)
    {
        msg = ex.Message;
        status = BsResponseStatus.ServerError;
    }

    return new BsJsonResult(new
    {
        Count = count,
        Html = html
    }, status, msg);
}


            

After we implement these actions, our controller is ready. Next we have to create the views.

4. Creating the Views

In this step we will create the Grid Views. The _Grid View will be implemented as a partial view rendered inside the Index View.

Let’s first create the Index View. To do this right click on the Index() Action in the Controller and choose ‘Add View’.

This is how our index.cshtml should look like:

@model MyGrid.Models.MoviesViewModel
@using BForms.Html

@{
    ViewBag.Title = "Movies Grid";
}

@using (Html.BsGridWrapper())
{
    @Html.Partial("Grid/_Grid", Model)
}

            

This view is just a container for the grid view. Let’s now create the _Grid partial view. Here we use the BsGridFor() html helper to create our grid. This helper returns a BsGridHtmlBuilder used to build and configure the html of the grid. This way we can set the grid’s properties, add attributes on the rows, add and modify columns, set grid’s color theme, add bulk actions, etc. This is our grid partial view:

@model MyGrid.Models.MoviesViewModel
@using BForms.Html
@using BForms.Grid
@using BForms.Models


@(
Html.BsGridFor(m => m.Grid)
.ConfigureColumns(cols =>
{
    cols.For(c => c.ReleaseDate)
        .Name("Released")
        .Text(x => String.Format("{0:MMM yyyy}", x.ReleaseDate));
    cols.For(c => c.WeekendRevenue)
        .Text(x => x.WeekendRevenue + " mil$");
    cols.For(c => c.GrossRevenue)
        .Text(x => x.GrossRevenue + " mil$");
    cols.For(c => c.Recommended)
        .Text(x => x.Recommended ? Html.BsGlyphicon(Glyphicon.ThumbsUp).ToHtmlString() : Html.BsGlyphicon(Glyphicon.ThumbsDown).ToHtmlString());
})
.PagerSettings(new BsPagerSettings
     {
        Size = 5,
        ShowFirstLastButtons = true,
        ShowPrevNextButtons = true,
        HasPagesText = true,
        HasPageSizeSelector = true
     })
)

            

To keep it simple for the moment we only set some properties for the pager with the help of the PagerSettings() method, and we modify two columns with the help of ConfigureColumns() method.

In the ConfigureColumns() method we change the display header of the column from ReleaseDate to Year with the help of the Name() method, and we also change the text so it displays only the Year property of the ReleaseDate. For the Recommended column, we change the display text from true/false to Yes/No.

In the PagerSettings() method we set the number of rows that should be displayed in the grid and we customize the look by setting some of the properties to true. With these settings the pager should look like this:


At this moment we still have to initialize the grid in javascript. We’ll do this next.

5. Initializing the Grid Widget

In this step we will create the javascript file that will be executed when the index page loads. There are two ways of working with the BForms JS components. One way is to use RequireJS.NET and the other is to reference the js files directly from the ~/Scripts/BForms/Bundles/js folder. In this example we’ll use the RequireJS way. For this let’s create the folder structure that the RequireJs expects for our views.


Inside the Scripts folder create the following folder structure corresponding to our Home Controller, Index page: Controllers/Root/Home/home-index.js

This is how the javascript file will look:

require([
        'jquery',
        'bforms-grid',
        'bootstrap'
], function () {
    var homeIndex = function (options) {
        this.options = $.extend(true, {}, options);
        this.init();
    };
    
    homeIndex.prototype.init = function () {
        this.$grid = $('#grid');
        this.initGrid();
    };
    
    homeIndex.prototype.initGrid = function() {
        this.$grid.bsGrid({
            uniqueName: 'moviesGrid',
            pagerUrl: this.options.pagerUrl
        });
    };

    $(document).ready(function () {
        var page = new homeIndex(window.requireConfig.pageOptions.index);
    });
});


            

In this javascript file we require bforms-grid, jquery and bootstrap. We use the jQuery document ready method to initialize our page and to get the options send from the HomeController. As you remember we have send as an option the ‘pageUrl’, which is a link to the action that needs to be called by the pager, to get a different page or to change the number of entries displayed.

On the page initialization we also find the grid by the id attribute and initialize it with the minimum required parameters. We only set a uniqueName and the pagerUrl properties. We will later set more properties as we enhance the functionality of our grid.

Now we have a functional grid that we can see in action.


Next step will be to enable details for the rows. This way we can modify the data and submit the changes to the server.

6. Enable Row Details

In this step we will modify our code to support row editing. For this we'll have to add a Details Model, create Details partial views, add new Actions to our Controller and add new options to our javascript Grid Widget.

The Details Model

First step is to create the MovieDetailsModel.

    public class MovieDetailsModel
    {
        public MovieDetailsModel()
        {
            IsRecommendedRadioButton = new BsSelectList<YesNoValueTypes>();
            IsRecommendedRadioButton.ItemsFromEnum(typeof(YesNoValueTypes), YesNoValueTypes.Both);
            IsRecommendedRadioButton.SelectedValues = YesNoValueTypes.Both;
        }

        public int Id { get; set; }

        [Required]
        [Display(Name = "Title")]
        [BsControl(BsControlType.TextBox)]
        public string Title { get; set; }

        [Required]
        [Display(Name = "Weekend Revenue")]
        [BsControl(BsControlType.Number)]
        public decimal WeekendRevenue { get; set; }

        [Display(Name = "Gross Revenue")]
        [BsControl(BsControlType.Number)]
        public decimal GrossRevenue { get; set; }

        [Required]
        [Display(Name = "Release Date")]
        [BsControl(BsControlType.DatePicker)]
        public BsDateTime ReleaseDate { get; set; }

        [Display(Name = "Genres")]
        [BsControl(BsControlType.ListBox)]
        public BsSelectList<List<int>> GenresList { get; set; }
        public string Genres { get; set; }

        [Display(Name = "Rating")]
        [BsControl(BsControlType.Number)]
        public double Rating { get; set; }

        [Required]
        [Display(Name = "Recomended")]
        [BsControl(BsControlType.RadioButtonList)]
        public BsSelectList<YesNoValueTypes> IsRecommendedRadioButton { get; set; }
        public bool Recommended { get; set; }

        [Display(Name = "Poster Image")]
        [BsControl(BsControlType.Url)]
        public string Poster { get; set; }
    }
            

You should include in your DetailsModel all the properties from your base model that you want to modify. The details view will enable us to edit these properties.

A good approach would be to split our properties in multiple parts, to reduce the data send to the server each time a user wants to modify a property. Let’s say we want to change only the WeekendRevenue and the GrossRevenue of our model. If we keep all the properties as fields in the same form, all the data will be send to the server on each request. If we separate the fields in multiple forms on the details view, only part of the data will be send. In our case we will have our model split in two parts: Info and Revenue.

To implement this we need an enum that contains all the parts. This is how our EditComponents enum will look:

    public enum EditComponents
    {
        Info = 1,
        Revenue = 2
    }
            

The Info view will contain the Title, Poster, Genre and Rating fields.

The Revenue view will contain the WeekendRevenue, GrossRevenue and ReleaseDate fields.

The Row Model

We also need to change the MoviesRowModel to inherit from the BsGridRowModel<MovieDetailsModel>. We'll need this in the _Grid Partial View to create a BsGridHtmlBuilder object. The inherited class has an abstract method named GetUniqueID(), so we have to override this method to return the unique id for the Row Model.

public class MoviesRowModel : BsGridRowModel<MovieDetailsModel>
    {
        public int Id { get; set; }

        [BsGridColumn(Width = 4, IsEditable = true, IsSortable = true)]
        public string Title { get; set; }

        [BsGridColumn(Width = 2, IsEditable = false, IsSortable = true)]
        public decimal WeekendRevenue { get; set; }

        [BsGridColumn(Width = 2, IsEditable = false, IsSortable = false)]
        public decimal GrossRevenue { get; set; }

        [BsGridColumn(Width = 2, IsEditable = false, IsSortable = true)]
        public DateTime ReleaseDate { get; set; }

        [BsGridColumn(Width = 2, IsEditable = false, IsSortable = true)]
        public bool Recommended { get; set; }

        public override object GetUniqueID()
        {
            return Id;
        }
    }
            

The View Model

The final change we make to our models is to set the HasDetails parameter to true on the MoviesViewModel's Grid property.

public class MoviesViewModel
{
    [BsGrid(HasDetails = true, Theme = BsTheme.Blue)]
    [Display(Name = "Top Movies")]
    public BsGridModel<MoviesRowModel> Grid { get; set; }
}


            

The Detail Views

Now that we have the Models we can move on to the Views. Before adding the details partial views we need to make some changes to the _Grid partial view.

We use an overloaded method for Html.BsGridFor() helper that takes two parameters. The first one is the Grid Model, and we pass it from our View Model. The second parameter is a BSGridHtmlBuilder object. We need to create this object first.

Here is the code that needs to be modified on the _Grid partial view:

@{
    var builder = new BsGridHtmlBuilder<MoviesViewModel, MoviesRowModel>();
}  

@Html.BsGridFor(m => m.Grid, builder)
            

We can now use the ConfigureRows() method to customize the rows. We set a details template, then we add objid as a custom data attribute, and finaly we set a row highliter based on the Recommended property of our model.

.ConfigureRows(cfg => cfg.HasCheckbox(row => row.Recommended)
                                   .DetailsTemplate(row => Html.Partial("Grid/Details/_Index", row.Details).ToString())
                                   .HtmlAttributes(row => new Dictionary<string, object> { { "data-objid", row.Id }, { "data-active", row.Recommended } })
                                   .Highlighter(row => row.Recommended ? "#59b444" : "#f0ad4e"))
            

In the previous step we have set the DetailsTemplate for the rows to Grid/Details/_Index. We also pass the MovieDetailsModel. Now we have to create this template.

As you know we have split our details model in two parts: Info and Revenue. This will correspond to two partial views named _Info and _Revenue. Each of these views will include a Readonly and an Editable partial views.

The final structure of the Views Folder will look like this:


In the next part we'll take a look at the code for each view.

The Details Index View

The _Index view includes the _Info and the _Revenue partial views using the Html.BsPartialPrefixed() helper method.

Also in this view we add two buttons that will be available in each row. The State Button which will toggle the Recommended property of our model, and the Delete Button which will delete the row from the grid and from the database.

The classes used on this button elements will be later used in the javascript file to identify the buttons and to attach event handlers on them. We used js-btn_state for the Recommended Button, and js-btn_delete for the Delete Button.

@using BForms.Html
@model MyGrid.Models.MovieDetailsModel

<div class="row grid_row_details" data-rowdetailsid="@Model.Id">
    <div class="col-lg-6">
        <div id="movies_info" class="grid_details description js-editableInfo">

            @Html.BsPartialPrefixed(x => x, "Grid/Details/_Info", Model, "x" + Model.Id.ToString())

        </div>
    </div>
    
    <div class="col-lg-6">
        <div id="movies_revenue" class="grid_details description js-editableRevenue">

            @Html.BsPartialPrefixed(x => x, "Grid/Details/_Revenue", Model, "x" + Model.Id.ToString())

        </div>
    </div>

    <div class="col-lg-12 bs-row_controls">
        <hr />
        @{
            var stateText = Model.Recommended ? "Not Recommended" : "Recommended";
            var recommended = Model.Recommended;
        }
        <button type="button" class="btn btn-warning js-btn_state" data-recommended="@recommended.ToString().ToLower()">@stateText</button>
        <button type="button" class="btn btn-danger pull-right js-btn_delete">Delete</button>
    </div>
</div>
            

This is how a expanded row will look like at the end:


We still need to implement our _Info and Revenue pages.

The _Info view creates an editable widget and loads the _InfoReadonly and _InfoEditable partial views. By default the Readonly page is displayed. To enable editing the user has to click the 'Pencil' icon in the <h3> that has the class open-editable.

@using BForms.Html
@using BForms.Models
@model MyGrid.Models.MovieDetailsModel

<h3 class="editable">
    @Html.BsGlyphicon(Glyphicon.Film)
    Info
    @Html.BsGlyphicon(Glyphicon.Pencil, new Dictionary<string, object> { { "class", "open-editable" } })
</h3>

@Html.Partial("Grid/Details/_InfoReadonly", Model)

@Html.Partial("Grid/Details/_InfoEditable", Model)
            

The _InfoReadonly partial view shows the Poster, Title, Rating and GenresList properties of our MoviesDetailsModel.

@using BForms.Html
@model MyGrid.Models.MovieDetailsModel

<div class="bs-readonly">
    <div class="col-sm-2 col-lg-2">
        <img src="@Model.Poster" alt="@Model.Title" height="150">
    </div>
    <div class="col-sm-10 col-lg-10">
        @if (!string.IsNullOrEmpty(Model.Title))
        {
            <dl>
                <dt>Title &nbsp;</dt>
                <dd>@Model.Title</dd>
            </dl>
        }
        @if (Model.Rating > 0)
        {
            <dl>
                <dt>Rating &nbsp;</dt>
                <dd>@Model.Rating</dd>
            </dl>
        }
        @if (Model.GenresList.SelectedValues.Count > 0)
        {
            <dl>
                <dt>Genre &nbsp;</dt>
                <dd>
                    <ul>
                        @foreach (var item in Model.GenresList.SelectedValues)
                        {
                            <li>@Model.GenresList.Items.Where(i => i.Value == item.ToString()).Select(x => x.Text).FirstOrDefault()</li>
                        }
                    </ul>
                </dd>
            </dl>
        }
    </div>
</div>
            

_InfoEditable partial views uses some BForms helpers to render the form fields.

@using BForms.Html
@using BForms.Models
@model MyGrid.Models.MovieDetailsModel

@using (Html.BsBeginForm())
{
    <div class="col-sm-12 col-lg-12 form-group @Html.BsValidationCssFor(m => m.Title)">
        @Html.BsLabelFor(m => m.Title)
        <div class="input-group">
            @Html.BsGlyphiconAddon(Glyphicon.FacetimeVideo)
            @Html.BsInputFor(m => m.Title)
            @Html.BsValidationFor(m => m.Title)
        </div>
    </div>
    
    <div class="col-sm-12 col-lg-12 form-group @Html.BsValidationCssFor(m => m.Rating)">
        @Html.BsLabelFor(m => m.Rating)
        <div class="input-group">
            @Html.BsGlyphiconAddon(Glyphicon.Star)
            @Html.BsInputFor(m => m.Rating)
            @Html.BsValidationFor(m => m.Rating)
        </div>
    </div>
    
    <div class="col-sm-12 col-lg-12 form-group @Html.BsValidationCssFor(m => m.GenresList)">
        @Html.BsLabelFor(m => m.GenresList)
        <div class="input-group">
            @Html.BsSelectFor(m => m.GenresList)
            @Html.BsValidationFor(m => m.GenresList)
        </div>
    </div>
    
    <div class="col-sm-12 col-lg-12 form-group @Html.BsValidationCssFor(m => m.Poster)">
        @Html.BsLabelFor(m => m.Poster)
        <div class="input-group">
            @Html.BsGlyphiconAddon(Glyphicon.Picture)
            @Html.BsTextBoxFor(m => m.Poster)
            @Html.BsValidationFor(m => m.Poster)
        </div>
    </div>
}
            

The code to save, delete or toggle the Recommended property will be later implemented in the Controller.

The _Revenue view is similar.

The Controller

In order to display the details on our page we have to make an ajax request to the server. The server will return the html used to render the details. For this we have to add a GetRows() Action to our controller.

This is how the GetRows() Action will look:

public BsJsonResult GetRows(List<BsGridRowData<int>> items)
{
    var msg = string.Empty;
    var status = BsResponseStatus.Success;
    var rowsHtml = string.Empty;

    try
    {
        rowsHtml = GetRowsHtml(items);
    }
    catch (Exception ex)
    {
        msg = "<strong>Server Error!</strong> " + ex.Message;
        status = BsResponseStatus.ServerError;
    }

    return new BsJsonResult(new
    {
        RowsHtml = rowsHtml
    }, status, msg);
}

[NonAction]
private string GetRowsHtml(List<BsGridRowData<int>> items)
{
    var ids = items.Select(x => x.Id).ToList();
    var rowsModel = _gridRepository.ReadRows(items.Select(x => x.Id).ToList());
    var viewModel = _gridRepository.ToBsGridViewModel(rowsModel, row => row.Id, items).Wrap<MoviesViewModel>(x => x.Grid);

    return this.BsRenderPartialView("Grid/_Grid", viewModel);
}
            

GetRows() Action calls GetRowsHtml() NonAction, to return the details for the requested rows. GetRowsHtml() calls ReadRows() in the repository.

The ReadRows() method gets the entities from the database by their Ids, converts them to RowModel and returns them as a List.

public List<MoviesRowModel> ReadRows(List<int> objIds)
{
    return db.Movies.Where(x => objIds.Contains(x.Id)).Select(MapMovie_MovieRowModel).ToList();
}
            

Finally we have to add the getRowsUrl and editComponents options to the RequireJs options dictionary. This way we can access them from the javascript file.

Here is how the options dictionary will look like for the Index() Action:

var options = new Dictionary<string, object>
{
    {"pagerUrl", Url.Action("Pager")},
    {"getRowsUrl", Url.Action("GetRows")},
    {"editComponents", RequireJsHtmlHelpers.ToJsonDictionary<EditComponents>()}
};

RequireJsOptions.Add("index", options);
            

The Details JavaScript

In this section we'll write the code we need in our home-index.js script file to display the details for each row.

Now that we have the getRowsUrl from the controller we can pass it to our grid. We also need to pass two handles that will be called before and after the Row Details ajax call is successful. The grid properties that need to be set are beforeRowDetailsSuccess and afterRowDetailsSuccess.

homeIndex.prototype.initGrid = function() {
    this.$grid.bsGrid({
        uniqueName: 'moviesGrid',
        pagerUrl: this.options.pagerUrl,
        detailsUrl: this.options.getRowsUrl,
        beforeRowDetailsSuccess: $.proxy(this._beforeDetailsSuccessHandler, this),
        afterRowDetailsSuccess: $.proxy(this._afterDetailsSuccessHandler, this)
    });
};


            

The _beforeDetailsSuccessHandler sets the options for the editable views, like the updateUrl and editSuccessHandler, by calling the _editableOptions() method.

The _afterDetailsSuccessHandler validates the returned row.

Here is the code that needs to be implemented:

 homeIndex.prototype._beforeDetailsSuccessHandler = function (e, data) {
        var $row = data.$row,
            response = data.data;

        var infoOpt = this._editableOptions($row, this.options.editComponents.Info);
        $row.find('.js-editableInfo').bsEditable(infoOpt);

        var revenueOpt = this._editableOptions($row, this.options.editComponents.Revenue);
        $row.find('.js-editableRevenue').bsEditable(revenueOpt);
    };

    homeIndex.prototype._editableOptions = function ($row, componentId) {
        return $.extend(true, {}, {
            url: this.options.updateUrl,
            prefix: 'x' + $row.data('objid') + '.',
            additionalData: {
                objId: $row.data('objid'),
                componentId: componentId
            },
            editSuccessHandler: $.proxy(function (editResponse) {
                this.$grid.bsGrid('updateRows', editResponse.RowsHtml);
            }, this)
        });
    };

    homeIndex.prototype._afterDetailsSuccessHandler = function (e, data) {
        var $row = data.$row;

        $row.find('.js-editableInfo').bsEditable('initValidation');
        $row.find('.js-editableRevenue').bsEditable('initValidation');
    };

            

For the validation we need to require more scripts before this page loads. The scripts that we need are bforms-validate, bforms-validate-unobtrusive and also bforms-ajax.

require([
        'jquery',
        'bforms-namespace',
        'bforms-grid',
        'bootstrap',
        'bforms-validate',
        'bforms-validate-unobtrusive',
        'bforms-ajax'
], function () {
      //...
});


            

The _editableOptions() method needs to know the updateUrl. This url will be called when the user clicks the save button in the editable view. We also need to implement the Update() Action in the Controller.

7. Editing and Updating the Rows

To update the changes made to a row the grid will make an ajax call to the Update() Action on the Controller.

public BsJsonResult Update(MovieDetailsModel model, int objId, EditComponents componentId)
{
            
    var msg = string.Empty;
    var status = BsResponseStatus.Success;
    var html = string.Empty;

            
    try
    {
        ClearModelState(ModelState, componentId);

        if (ModelState.IsValid)
        {
            var detailsModel = _gridRepository.Update(model, objId, componentId);


            switch (componentId)
            {
                case EditComponents.Info:
                    html = this.BsRenderPartialView("Grid/Details/_InfoReadonly", detailsModel);
                    break;
                case EditComponents.Revenue:
                    html = this.BsRenderPartialView("Grid/Details/_RevenueReadonly", detailsModel);
                    break;
            }

            var rowModel = _gridRepository.ReadRow(objId);

            var viewModel = _gridRepository.ToBsGridViewModel(rowModel, true).Wrap<MoviesViewModel>(x => x.Grid);

            html = this.BsRenderPartialView("Grid/_Grid", viewModel);
        }
    }
    catch (Exception ex)
    {
        msg = "<strong>Server Error!</strong> " + ex.Message;
        status = BsResponseStatus.ServerError;
    }
            

    return new BsJsonResult(new
    {
        RowsHtml = html
    }, status, msg);
}

            

The Update() Action receives the modified MovieDetailsModel and the EditComponents componentId. It clears the state of the model based on the component id by calling the ClearModelState NonAction in the same controller.

This is how the ClearModelState() NonAction is implemented:

[NonAction]
public void ClearModelState(ModelStateDictionary ms, EditComponents componentId)
{
    switch (componentId)
    {
        case EditComponents.Info:
            ms.ClearModelState(new List<string>() { "Title", "Poster", "Rating", "GenresList" });
            break;
        case EditComponents.Revenue:
            ms.ClearModelState(new List<string>() { "GrossRevenue", "WeekendRevenue", "ReleaseDate" });
            break;
    }
}

            

After the model state is cleared, the Update() method in the repository is called. This will save the changes in the database and will return the details model after it fills the dropdowns and lists used in the model.

This is the code for the Update() method in the Repository.

public MovieDetailsModel Update(MovieDetailsModel model, int objId, EditComponents componentId)
{
    var entity = db.Movies.FirstOrDefault(x => x.Id == objId);

    if (entity != null)
    {
        switch (componentId)
        {
            case EditComponents.Info:
                entity.Title = model.Title;
                entity.IsRecommended = model.IsRecommendedRadioButton.SelectedValues == YesNoValueTypes.Yes ? true : false;
                if(!string.IsNullOrEmpty(model.Poster))
                    entity.Poster = model.Poster;
                entity.Rating = model.Rating;
                entity.Genres = string.Join(",",model.GenresList.SelectedValues);
                break;
            case EditComponents.Revenue:
                entity.GrossRevenue = model.GrossRevenue;
                entity.WeekendRevenue = model.WeekendRevenue;
                if (model.ReleaseDate.DateValue.HasValue)
                    entity.ReleaseDate = model.ReleaseDate.DateValue.Value;
                break;
        }
        db.SaveChanges();
    }

    return FillDetailsProperties(MapMovie_MovieDetailsModel(entity));
}

public MovieDetailsModel FillDetailsProperties(MovieDetailsModel detailsModel)
{
    detailsModel.IsRecommendedRadioButton.SelectedValues = detailsModel.Recommended ? YesNoValueTypes.Yes : YesNoValueTypes.No;
    detailsModel.GenresList = new BsSelectList<List<int>>();
    detailsModel.GenresList.ItemsFromEnum(typeof(MovieGenre));
    detailsModel.GenresList.SelectedValues = new List<int>();
    var options = detailsModel.Genres.Split(',').ToList();
    foreach (string option in options)
    {
        MovieGenre genre;
        if (Enum.TryParse(option, true, out genre))
            detailsModel.GenresList.SelectedValues.Add((int)genre);
    }

    return detailsModel;
}
            

Also in the Update() Action we call the ReadRow() method in the repository to return the row model.

public MoviesRowModel ReadRow(int objId)
{
    return db.Movies.Where(x => x.Id == objId).Select(MapMovie_MovieRowModel).FirstOrDefault();
}
            

We also need to add the updateUrl to the RequireJs options.

var options = new Dictionary<string, object>
{
    {"pagerUrl", Url.Action("Pager")},
    {"getRowsUrl", Url.Action("GetRows")},
	{"editComponents", RequireJsHtmlHelpers.ToJsonDictionary<EditComponents>()},
	{"updateUrl", Url.Action("Update")}
};

RequireJsOptions.Add("index", options);
            

8. Row Actions

We want to add two buttons to each row. One to toggle the Recommended property and one to delete the row. The buttons will be visible when the row is expanded.


To implement the Delete and Recommended buttons we have to provide to the grid an array of rowAction objects. We implement this in the home-index.js file. This is how the initGrid method will look at the end:

    homeIndex.prototype.initGrid = function() {
        this.$grid.bsGrid({
            uniqueName: 'moviesGrid',
            pagerUrl: this.options.pagerUrl,
            detailsUrl: this.options.getRowsUrl,
            beforeRowDetailsSuccess: $.proxy(this._beforeDetailsSuccessHandler, this),
            afterRowDetailsSuccess: $.proxy(this._afterDetailsSuccessHandler, this),
            rowActions: [{
                btnSelector: '.js-btn_state',
                url: this.options.recommendUnrecommendUrl,
                handler: $.proxy(this._recommendUnrecommendHandler, this),
            }, {
                btnSelector: '.js-btn_delete',
                url: this.options.deleteUrl,
                init: $.proxy(this._deleteHandler, this),
                context: this
            }]
        });
    };
            

The rowActions option takes an array of json objects. One object for each button. In each object we set the url for the action on the server, the selector for the button element and the handler.

Here are the handlers for the Delete and Recommended buttons in the javascript file:

homeIndex.prototype._recommendUnrecommendHandler = function (e, options, $row, context) {

    var data = [];

    data.push({
        Id: $row.data('objid'),
        GetDetails: $row.hasClass('open')
    });

    this._ajaxRecommendUnrecommend($row, data, options.url, function (response) {

        context.updateRows(response.RowsHtml);

    }, function (response) {
        context._rowActionAjaxError(response, $row);
    });

};

homeIndex.prototype._ajaxRecommendUnrecommend = function ($html, data, url, success, error) {
    var ajaxOptions = {
        name: '|recommendUnrecommend|' + $html.data('objid'),
        url: url,
        data: data,
        context: this,
        success: success,
        error: error,
        loadingElement: $html,
        loadingClass: 'loading'
    };
    $.bforms.ajax(ajaxOptions);
};

homeIndex.prototype._deleteHandler = function (options, $row, context) {

    //add popover widget
    var $me = $row.find(options.btnSelector);
    $me.popover({
        html: true,
        placement: 'left',
        content: $('.popover-content').html()
    });

    // add delegates to popover buttons
    var tip = $me.data('bs.popover').tip();
    tip.on('click', '.bs-confirm', $.proxy(function (e) {
        e.preventDefault();

        var data = [];
        data.push({
            Id: $row.data('objid')
        });

        this._ajaxDelete($row, data, options.url, function () {
            $row.remove();
        }, function (response) {
            context._rowActionAjaxError(response, $row);
        });

        $me.popover('hide');
    }, this));
    tip.on('click', '.bs-cancel', function (e) {
        e.preventDefault();
        $me.popover('hide');
    });
};

homeIndex.prototype._ajaxDelete = function ($html, data, url, success, error) {
    var ajaxOptions = {
        name: '|delete|' + data,
        url: url,
        data: data,
        context: this,
        success: success,
        error: error,
        loadingElement: $html,
        loadingClass: 'loading'
    };
    $.bforms.ajax(ajaxOptions);
};
            

The handlers make ajax calls to the server. We need to implement the Delete and the Recommended actions on the Controller.

public BsJsonResult Delete(List<BsGridRowData<int>> items)
{
    var msg = string.Empty;
    var status = BsResponseStatus.Success;

    try
    {
        foreach (var item in items)
        {
            _gridRepository.Delete(item.Id);
        }
    }
    catch (Exception ex)
    {
        msg = "<strong>Server Error!</strong> " + ex.Message;
        status = BsResponseStatus.ServerError;
    }

    return new BsJsonResult(null, status, msg);
}

public BsJsonResult RecommendUnrecommend(List<BsGridRowData<int>> items, bool? recommended)
{
    var msg = string.Empty;
    var status = BsResponseStatus.Success;
    var rowsHtml = string.Empty;

    try
    {
        foreach (var item in items)
        {
            _gridRepository.RecommendUnrecommend(item.Id, recommended);
        }

        rowsHtml = GetRowsHtml(items);
    }
    catch (Exception ex)
    {
        msg = "<strong>Server Error!</strong> " + ex.Message;
        status = BsResponseStatus.ServerError;
    }

    return new BsJsonResult(new
    {
        RowsHtml = rowsHtml
    }, status, msg);
}
            

We also need to implement the actual Delete and Recommend toggle methods in the repository.

public void Delete(int objId)
{
    var entity = db.Movies.FirstOrDefault(x => x.Id == objId);

    if (entity != null)
    {
        db.Movies.Remove(entity);
        db.SaveChanges();
    }
}

public void RecommendUnrecommend(int objId, bool? recommended)
{
    var entity = db.Movies.FirstOrDefault(x => x.Id == objId);

    if (entity != null)
    {
        entity.IsRecommended = recommended.HasValue ? recommended.Value : !entity.IsRecommended;
        db.SaveChanges();
    }
}
            

The final step is to add the deleteUrl and recommendUnrecommendUrl to the RequireJs options.

public ActionResult Index()
{
    var gridModel = _gridRepository.ToBsGridViewModel(new BsGridBaseRepositorySettings());

    var model = new MoviesViewModel
    {
        Grid = gridModel,
    };

    var options = new Dictionary<string, object>
    {
        {"pagerUrl", Url.Action("Pager")},
        {"getRowsUrl", Url.Action("GetRows")},
        {"recommendUnrecommendUrl", Url.Action("RecommendUnrecommend")},
        {"updateUrl", Url.Action("Update")},
        {"deleteUrl", Url.Action("Delete")},
        {"editComponents", RequireJsHtmlHelpers.ToJsonDictionary<EditComponents>()}
    };

    RequireJsOptions.Add("index", options);

    return View(model);
}
            

Now we have a functional grid. We can edit, delete and toggle the recommendation for each row. This is how the grid looks like with an expanded row.


9. Bulk Actions

Bulk Actions are actions applied to multiple rows at the same time. When Bulk Actions are enabled, a checkbox control is rendered next to each row. You can manually select multiple rows, or you can apply a filter.

In our Top Movies Grid we will filter the columns by the Recommended property. For example, we will be able to select all recommended Movies and call the delete bulk action to erase all the selected rows from the grid at the same time.

Adding Bulk Actions to the Grid View


To render the buttons on the page, we need to call two functions on the Html.BsGridFor() Helper. GridResetButton() to render a Bulk Reset Button and ConfigureBulkActions() to add three more buttons - Recommend, Unrecommend and Delete.


Html.BsGridFor(m => m.Grid)
.GridResetButton()
.ConfigureBulkActions(bulk =>
        {
            bulk.AddAction().StyleClass("btn-success js-btn-recommend_selected").Title("Recommend selected").GlyphIcon(Glyphicon.ThumbsUp);
            bulk.AddAction().StyleClass("btn-warning js-btn-unrecommend_selected").Title("Unrecommend selected").GlyphIcon(Glyphicon.ThumbsDown);
            bulk.AddAction(BsBulkActionType.Delete);

            bulk.AddSelector(BsBulkSelectorType.All);
            bulk.AddSelector().StyleClass("js-actives").Text("Recomended");
            bulk.AddSelector().StyleClass("js-inactives").Text("Unrecommended");
            bulk.AddSelector(BsBulkSelectorType.None);

            bulk.ForSelector(BsBulkSelectorType.All).Text("All");
            bulk.ForSelector(BsBulkSelectorType.None).Text("None");
        })

            

As you can see in the above code, we use AddAction() to add the buttons, and AddSelector() to add the options for the filter dropdown.

Here is how the grid should look like:


Bulk Actions Javascript

Now that we have the buttons added to the page we have to initialize them in the javascript file.

To initialize the Filter Selector we pass an array of objects to the filterButtons property of our grid widget, representing the selector and the filter function that will be called when the filter is selected.

To initialize the Bulk Action buttons we need to pass an array of objects to the gridActions property of our Movies Grid widget, representing the selector and handler for each button.

...
    homeIndex.prototype.initGrid = function() {
        this.$grid.bsGrid({
		...
            filterButtons: [{
                btnSelector: '.js-actives',
                filter: function ($el) {
                    return $el.data('active') == 'True';
                }
            }, {
                btnSelector: '.js-inactives',
                filter: function ($el) {
                    return $el.data('active') != 'True';
                },
            }],

            gridActions: [{
                btnSelector: '.js-btn-recommend_selected',
                handler: $.proxy(function ($rows, context) {
                    var data = {};

                    var items = context.getSelectedRows();

                    data.items = items;
                    data.recommended = true;

                    this._ajaxRecommendUnrecommend($rows, data, this.options.recommendUnrecommendUrl, function (response) {

                        context.updateRows(response.RowsHtml);

                    }, function (response) {
                        context._pagerAjaxError(response);
                    });
                }, this)
            }, {
                btnSelector: '.js-btn-unrecommend_selected',
                handler: $.proxy(function ($rows, context) {
                    var data = {};

                    var items = context.getSelectedRows();
                    data.items = items;
                    data.recommended = false;

                    this._ajaxRecommendUnrecommend($rows, data, this.options.recommendUnrecommendUrl, function (response) {

                        context.updateRows(response.RowsHtml);

                    }, function (response) {
                        context._pagerAjaxError(response);
                    });
                }, this)
            }, {
                btnSelector: '.js-btn-delete_selected',
                handler: $.proxy(function ($rows, context) {

                    var items = context.getSelectedRows();

                    this._ajaxDelete($rows, items, this.options.deleteUrl, $.proxy(function () {
                        $rows.remove();
                        context._evOnRowCheckChange($rows);
                        if (this.$grid.find('.grid_row[data-objid]').length == 0) {
                            this.$grid.bsGrid('refresh');
                        }
                    }, this), function (response) {
                        context._pagerAjaxError(response);
                    });
                }, this),
                popover: true
            }]
        });
    };
...

            

In the delete action we also set the popover option to true. This will enable the confirmation popup for the delete action.


Now the Bulk Actions are functional. On the server we call the existing methods for delete and recommend/unrecommend actions. If you implement a custom action you have to send the URL in the RequireJs Options from the controller.

10. Reorder Grid Columns and Save Order to Session State

The Grid Widget alows Grid Columns to be reordered by Drag and Drop the column header. However, to persist the new column order we have to save it to the Session and read it on page refresh.

Before Reordering Grid Columns


Drag And Drop Column Reorder


After Reordering Grid Columns


Let's make sure we store the new column order to the Session State so we can read it later. For this we have to make some changes to the HomeController.

Let's start by adding the SetGridSetting() and GetGridSettings() NonAction Helper methods to the Controller.

Get Set Grid Settings

[NonAction]
public void SaveGridSettings(BsGridRepositorySettings<MoviesSearchModel> settings)
{
    if (settings.OrderColumns != null)
    {
        Session["GridSettings"] = new BsGridSavedSettings
        {
            PageSize = settings.PageSize,
            OrderableColumns = settings.OrderableColumns,
            OrderColumns = settings.OrderColumns
        };
    }
}

[NonAction]
public BsGridSavedSettings GetGridSettings()
{
    return Session["GridSettings"] as BsGridSavedSettings;
}

[Serializable]
public class BsGridSavedSettings
{
    public int PageSize { get; set; }

    public List<BsColumnOrder> OrderableColumns { get; set; }

    public Dictionary<string, int> OrderColumns { get; set; }
}
            

We store the new columns order as GridSettings. We use the SetGridSettings() method to save the new order to Session State, and GetGridSettings() method to read the column order data from the Session.

The new column order is saved in the Pager() action. Let's see how we modified the Action to accomplish this.

Modified Pager Action

public BsJsonResult Pager(BsGridBaseRepositorySettings settings)
{
    ....

    try
    {
        SaveGridSettings(settings);

        var viewModel = _gridRepository.ToBsGridViewModel(settings, out count).Wrap<MoviesViewModel>(x => x.Grid);

        html = this.BsRenderPartialView("Grid/_Grid", viewModel);
    }
    catch (Exception ex)
    {
        msg = ex.Message;
        status = BsResponseStatus.ServerError;
    }

    ....
}
            

Inside Pager() Action we make a call to SaveGridSettings(settings). This way any changes to the grid made by the user will be saved to the Session State.

Now that we have the data saved, we can read it inside the Index() Action, and also inside GetRowsHtml().

Modified Index Action

public ActionResult Index()
{
    var bsGridSettings = new BsGridBaseRepositorySettings
    {
        Page = 1,
        PageSize = 5
    };

    var savedSettings = GetGridSettings();

    if (savedSettings != null)
    {
        bsGridSettings.OrderableColumns = savedSettings.OrderableColumns;
        bsGridSettings.OrderColumns = savedSettings.OrderColumns;
        bsGridSettings.PageSize = savedSettings.PageSize;
    }

    var gridModel = _gridRepository.ToBsGridViewModel(bsGridSettings);

    ....

    return View(model);
}
            

We use GetGridSettings() to get the saved settings from the Session State. If it exists we modify bsGridSettings object and send it as an argument to _gridRepository.ToBsGridViewModel().

We have to do the same inside GetRowsHtml() Action.

Modified Get Rows Html NonAction

[NonAction]
private string GetRowsHtml(List<BsGridRowData<int>> items)
{
    var ids = items.Select(x => x.Id).ToList();
    var rowsModel = _gridRepository.ReadRows(ids);
    var viewModel = _gridRepository.ToBsGridViewModel(rowsModel, row => row.Id, items).Wrap<MoviesViewModel>(x => x.Grid);

    var savedSettings = GetGridSettings();

    if (savedSettings != null)
    {
        viewModel.Grid.BaseSettings.OrderableColumns = savedSettings.OrderableColumns;
        viewModel.Grid.BaseSettings.OrderColumns = savedSettings.OrderColumns;
        viewModel.Grid.BaseSettings.PageSize = savedSettings.PageSize;
    }
     
    return this.BsRenderPartialView("Grid/_Grid", viewModel);
}
            

GetRowsHtml() NonAction is used inside GetRows() Action and also inside RecommendUnrecommend() Action.

Now the user can reorder grid columns, and the new order will persist on page refresh.


Building a Toolbar for the Grid

Now that we have a functional grid we can implement the BForms.Toolbar for it. The toolbar will have search functionality and will be able to filter the grid by using both Advanced Search and Quick Search. Later we will add functionality to create New entities and add them to the grid.

Initial Project

We will start from where we left with the Grid Tutorial and we will add the Toolbar with AdvancedSearch functionality. To add the toolbar we have to add new code and also modify existing code. You will see all the changes in the following steps. We will start by making changes to our models.

1. Creating the Toolbar and Search Models

In this step we will create the models needed to build the toolbar. Because we want to filter the grid, we add the search functionality to the toolbar. There are two built in ways to filter the grid: Advanced Search and Quick Search.

Advanced Search uses a custom partial view that will display inputs and controls, used to filter the grid on different properties, provided by us in the SearchModel.

Quick Search uses an input text placed directly on the toolbar, where the user can input text that will filter the grid on multiple model properties, as he types.

We will implement both, but we will start with Advanced Search.

The Search Model is used by the toolbar widget to populate the Advanced Search partial view. The fields can be annotated with the BsControlAttribute. This attribute will tell the view what type of control to render for that specific property.

Movie Search Model Class

public class MoviesSearchModel
{
    public MoviesSearchModel()
    {
        Recommended = new BsSelectList<YesNoValueTypes?>();
        Recommended.ItemsFromEnum(typeof(YesNoValueTypes));
        Recommended.SelectedValues = YesNoValueTypes.Both;
    }

    [Display(Name = "Title")]
    [BsControl(BsControlType.TextBox)]
    public string Title { get; set; }

    [Display(Name = "Release Date Interval")]
    [BsControl(BsControlType.DatePickerRange)]
    public BsRange<DateTime?> ReleaseDate { get; set; }

    [BsControl(BsControlType.RadioButtonList)]
    [Display(Name = "Recommended")]
    public BsSelectList<YesNoValueTypes?> Recommended { get; set; }
}
            

As you can see, we use only three properties to filter the grid, but you can use as many as you like. The actual filter functionality will be added later inside the repository.


Now we can modify the MoviesViewModel. We have to add the Toolbar property that will be of type BsToolbarModel<MoviesSearchModel>.

This is how the MoviesViewModel will look like:

Movies View Model Class

public class MoviesViewModel
{
    [BsGrid(HasDetails = true, Theme = BsTheme.Blue)]
    [Display(Name = "Top Movies")]
    public BsGridModel<MoviesRowModel> Grid { get; set; }

    [BsToolbar(Theme = BsTheme.Black)]
    [Display(Name = "Top Movies")]
    public BsToolbarModel<MoviesSearchModel> Toolbar { get; set; }
}
            

The BsToolbar attribute is used here to set the theme for the toolbar. The theme is set to Black in this case: [BsToolbar(Theme = BsTheme.Black)]. We can choose a different theme for the toolbar, regardless of the theme used for the grid.


2. Modifying the Controller

In this step we will modify the Controller, to accomodate the changes made to our models. The search data will be passed between the client an the server with the help of a settings object of type BsGridRepositorySettings. The settings object will contain information about the Grid Pager, Column Order, Quick Search and Advanced Search.

We will create the BsGridRepositorySettings object inside the Index() Action of the Controller. After we create it, we use it as a parameter when we create the initial grid model, used to be passed to the view. We will also instantiate a toolbar, and asign it to the MoviesViewModel - Toolbar property.

This is how the Index() Action should look like:

Index Action

public ActionResult Index()
{
    var bsGridSettings = new BsGridRepositorySettings<MoviesSearchModel>
    {
        Page = 1,
        PageSize = 5
    };

    var gridModel = _gridRepository.ToBsGridViewModel(bsGridSettings);

    var model = new MoviesViewModel
    {
        Grid = gridModel,
        Toolbar = new BsToolbarModel<MoviesSearchModel>
        {
            Search = _gridRepository.GetSearchForm()
        }
    };

    ...

    return View(model);
}
            

We use GetSearchForm() method from the repository to fill and initialize the Search form controls. We will add this method in the next step.

Also in the Controller we have to modify the Pager() Action to bind it to the setting object that will be sent via Ajax by the page. The settings object passed as parameter should be of type BsGridRepositorySettings<MoviesSearchModel>.

Pager Action

public BsJsonResult Pager(BsGridRepositorySettings<MoviesSearchModel> settings)
{
    ....
}
            

The settings object is serialized and send between the page and the controller. It contains QuickSearch, Advanced Search and Grid settings, and it is used in the Controller to build the database query on each request.


3. Modifying the Repository

The first method we need to add is GetSearchForm(). In this method you can populate dropdowns or other custom controls you might use. We use it to set up the Date Range Picker control.

GetSearchForm Repository Method

public MoviesSearchModel GetSearchForm()
{
    return new MoviesSearchModel
    {
        ReleaseDate = new BsRange<DateTime?>()
        {
            From = new BsRangeItem<DateTime?>
            {
                ItemValue = new DateTime(2013, 1, 1)
            },
            To = new BsRangeItem<DateTime?>
            {
                ItemValue = DateTime.Now
            }
        }
    };
}
            

Next step is to modify the Query() method in such way that it filters the results before returning the query.

Modified Query Method

public override IQueryable<Movie> Query()
{
    var query = db.Movies.AsQueryable();

    return Filter(query);
}

            

The Query() method calls the Filter() method before returning the query. This way the result will be filtered if the user submitted the Search Form.

This is how the Filter() method should look like:

Filter Method

public IQueryable<Movie> Filter(IQueryable<Movie> query)
{

    if (this.Settings != null)
    {
        if (this.Settings.Search != null)
        {
            // Release Date

            if (this.Settings.Search.ReleaseDate != null)
            {
                var fromDate = this.Settings.Search.ReleaseDate.From;
                var toDate = this.Settings.Search.ReleaseDate.To;

                if (fromDate.ItemValue.HasValue)
                {
                    query = query.Where(x => x.ReleaseDate >= fromDate.ItemValue.Value);
                }
                if (toDate.ItemValue.HasValue)
                {
                    query = query.Where(x => x.ReleaseDate <= toDate.ItemValue.Value);
                }
            }

            //Title

            if (!string.IsNullOrEmpty(this.Settings.Search.Title))
            {
                var title = this.Settings.Search.Title.ToLower();
                query = query.Where(x => x.Title.ToLower().Contains(title));
            }

            //Recommended

            if (this.Settings.Search.Recommended.SelectedValues.HasValue)
            {
                var isEnabled = this.Settings.Search.Recommended.SelectedValues.Value;

                if (isEnabled == YesNoValueTypes.Yes)
                {
                    query = query.Where(x => x.IsRecommended);
                }
                else if (isEnabled == YesNoValueTypes.No)
                {
                    query = query.Where(x => !x.IsRecommended);
                }
            }
        }
    }        

    return query;
}

            

The Filter() method checks if the Settings property is not null and also if it has Search data.

It then uses the Search data to filter the query. In this case we filter the query by ReleaseDate, Title and Recommended properties.

Last we should to add the Settings property for our repository:

Settings Property

public BsGridRepositorySettings<MoviesSearchModel> Settings
{
    get
    {
        return settings as BsGridRepositorySettings<MoviesSearchModel>;
    }
    set
    {
        settings = value;
    }
}
            

4. Adding the Toolbar and Search Views

Now it's time to add the Views for the Toolbar and for the Advanced Search. Inside the Views/Home folder create a new folder named Toolbar. Inside this folder create two partial views.

The first one will be the _Toolbar partial view, that will be included just above the grid view inside the index view.

The second view will be the _Search partial view that will contain all the fields for the Advanced Search, and will be included as an action inside the toolbar view.

This is how the folders structure and the views should look like:


Toolbar Partial View

@model MyGrid.Models.MoviesViewModel
@using BForms.Grid
@using BForms.Html

@(Html.BsToolbarFor(x => x.Toolbar)
    .DisplayName("Top Movies")
    .ConfigureActions(ca => ca.Add(BsToolbarActionType.AdvancedSearch).Tab(x => Html.BsPartialPrefixed(y => y.Search, "Toolbar/_Search", x)))
)
            

The toolbar view uses Html.BsToolbarFor(x => x.Toolbar) helper to render the toolbar. We can chain diferent functions to this helper to setup the toolbar the way we want.

In this case we used just DisplayName("Top Movies") to change the dysplay name, and ConfigureActions(ca => ca.Add(BsToolbarActionType.AdvancedSearch) to add a predefined AdvancedSearch button to the toolbar.

The panel opend by the AdvancedSearch button is set with the Tab(x => Html.BsPartialPrefixed(y => y.Search, "Toolbar/_Search", x)). This tells the toolbar to use the _Search partial view for the AdvancedSearch Tab.

There are predefined BsToolbarActionType for QuickSearch, AdvancedSearch, New and Order, but you can add your own custom action or use an ActionLink.


Search Partial View

@using BForms.Html
@using BForms.Models
@using MyGrid.Mock
@model MyGrid.Models.MoviesSearchModel

@using (Html.BsBeginForm())
{
    <div class="col-sm-12 col-lg-12 form-group">
        @Html.BsLabelFor(m => m.Title)
        <div class="input-group">
            @Html.BsGlyphiconAddon(Glyphicon.FacetimeVideo)
            @Html.BsInputFor(m => m.Title)
        </div>
    </div>
    
    <div class="col-sm-6 col-lg-6 form-group">
        @Html.BsLabelFor(m => m.ReleaseDate)
        <div class="input-group">
            @Html.BsGlyphiconAddon(Glyphicon.Calendar)
            @Html.BsRangeFor(model => model.ReleaseDate)
        </div>
    </div>
   
    <div class="col-sm-6 col-lg-6 form-group">
        @Html.BsLabelFor(m => m.Recommended)
        <div class="input-group">
            @Html.BsSelectFor(model => model.Recommended, new Dictionary<string, object>() { { "data-initialvalue", (byte)YesNoValueTypes.Both } }, null)
        </div>
    </div>

    <div class="col-sm-12 col-md-12 col-lg-12 grid_toolbar_submit">
        <button type="submit" class="btn btn-default js-btn-search">Search</button>
        <a href="#" class="js-btn-reset">Reset</a>
    </div>
}
            

The _Search partial view uses Html.BsBeginForm() to render a form that contains a field for each of our search model properties. We use BForms controlls like BsRangeFor() and .BsSelectFor.

Also the view contains two buttons, one for submitting the search data to the server and one to cancel and reset the form fields. The buttons will be referenced in the JavaScript files with the help of the js-btn-search and js-btn-reset classes.


Modified Index View

@model MyGrid.Models.MoviesViewModel
@using BForms.Html

@{
    ViewBag.Title = "Movies Grid";
}


@using (Html.BsGridWrapper())
{
    @Html.Partial("Toolbar/_Toolbar", Model)
    
    @Html.Partial("Grid/_Grid", Model)
}
            

The modified Index View now includes the Html.Partial("Toolbar/_Toolbar", Model) just above the grid partial view, inside the Html.BsGridWrapper().

If we build the project, the toolbar will be shown, but it won't be functional yet. We still have to initialize it inside the javascript file.


5. Initializing the Toolbar Widget

Now that we have the views we can initialize the Toolbar and the Toolbar Actions in the javascript file.

Inside the Scripts folder, modify the Controllers/Root/Home/home-index.js to initialize the Toolbar Widget and to add the search functionality.

Modified Home Index Javascript

require([
        'jquery',
        'bforms-namespace',
        'bforms-grid',
        'bforms-toolbar',
        'bootstrap',
        'bforms-ajax'
], function () {

	....
    
    homeIndex.prototype.init = function () {
        this.$grid = $('#grid');
        this.$toolbar = $('#toolbar');
        
        this.initGrid();
        this.initToolbar();
    };
	    
	....

	homeIndex.prototype.initToolbar = function () {
        this.$toolbar.bsToolbar({
            uniqueName: 'moviesToolbar',
            subscribers: [this.$grid]
        });
    };

	....

});
            

In the code snippet above are included only the modified parts.

First we have to include 'bforms-toolbar' in the require() function. This way the bforms-toolbar javascript code will be loaded before we execute the code in our page.

Next we use jquery to reference the toolbar element this.$toolbar = $('#toolbar'); and we call the this.initToolbar(); function.

The initToolbar() function uses .bsToolbar() method to initialize the toolbar widget and to set the initial properties. In this example we only set the minimum necessary, but it will get more complicated as we add more and more functionality to our toolbar.

Now the toolbar is functional and we can use the Advanced Search to filter the grid rows. This is how the toolbar should look like with the search tab open:


6. Adding QuickSearch Functionality

QuickSearch will be represented by a textbox rendered inside the toolbar. When the user wants to search after a keyword in any of the grid columns, the grid will be filtered as he types.

The QuickSearch text will be added to the Settings object, and will be sent via Ajax to the Controller. We need to modify our query inside the repository to take into consideration this QuickSearch string.

We also have to modify the _Toolbar view to add the QuickSearch control to the toolbar.

The QuickSearch and AdvancedSearch will be initialized automaticaly inside the javascript file. So we won't change the js file in this step. You only have to initialize manually any custom controls you might add to the toolbar.

Lets start by modifying the _Toolbar view.

Modified Toolbar View

@model MyGrid.Models.MoviesViewModel
@using BForms.Grid
@using BForms.Html

@(Html.BsToolbarFor(x => x.Toolbar)
    .DisplayName("Top Movies")
    .ConfigureActions(ca =>
    {
        ca.Add<BsToolbarQuickSearch>().Placeholder("Search");
        
        ca.Add(BsToolbarActionType.AdvancedSearch).Tab(x => Html.BsPartialPrefixed(y => y.Search, "Toolbar/_Search", x));
    })
)
            

We use Add<BsToolbarQuickSearch>() helper inside the ConfigureActions() method to add the QuickSearch control to the toolbar.

The Placeholder() helper is used to set the placeholder attribute of the QuickSearch TextBox. You can use any string as a parameter, or a Resource if you want to localize the control.

This is how the toolbar should look like now:


The QuickSearch Textbox is not functional yet. We have to modify the Filter() method inside the Repository.

Modified Filter Method

 public IQueryable<Movie> Filter(IQueryable<Movie> query)
{

    if (this.Settings != null)
    {
        if (!string.IsNullOrEmpty(Settings.QuickSearch))
        {
            var searched = Settings.QuickSearch.ToLower();

            var queryQuick = query.Where(x => x.Title.ToLower().Contains(searched) ||
                                                x.Genres.ToLower().Contains(searched));
            query = queryQuick.Select(x => x);
        }
        else if (this.Settings.Search != null)
        {
           
		   ....

        }
    }        

    return query;
}
            

We added another test to see if the user typed something into the QuickSearch field. If yes, we capture the searched term and filter the results. In this example we test if the search term matches any title or movie genre. You can use any property of the Movie Entity.

If no QuickSearch term is present it will test if there is any Advanced Search data submitted. It won't use both QuickSearch and AdvancedSearch at the same time.

Here is how the grid looks like when we type into the QuickSearch TextBox the "Comedy" genre:


7. Adding New Entities

An important functionality we can add to the toolbar is the abillity to add new rows to the grid. We will use an Add button in the toolbar to open a new form. When the form is submitted, the new Movie entity is added to the database, and the grid is refreshed.

For this we need to add a new model and view and then to make some changes to the controller.

We start by adding the MoviesNewModel that will be used to fill the New form, and send the data to the server.

Movies New Model Class

 public class MoviesNewModel
 {
     public MoviesNewModel()
     {
         Recommended = new BsSelectList<YesNoValueTypes?>();
         Recommended.ItemsFromEnum(typeof(YesNoValueTypes), YesNoValueTypes.Both);
         Recommended.SelectedValues = YesNoValueTypes.Yes; 
     }

     [Required]
     [Display(Name = "Movie Title")]
     [BsControl(BsControlType.TextBox)]
     public string Title { get; set; }

     [Display(Name = "Weekend Revenue")]
     [BsControl(BsControlType.TextBox)]
     public decimal WeekendRevenue { get; set; }

     [Display(Name = "Gross Revenue")]
     [BsControl(BsControlType.TextBox)]
     public decimal GrossRevenue { get; set; }

     [Required]
     [Display(Name = "Release Date")]
     [BsControl(BsControlType.DateTimePicker)]
     public BsDateTime ReleaseDate { get; set; }

     [Display(Name = "Genres", Prompt = "Select movie genres")]
     [BsControl(BsControlType.ListBox)]
     public BsSelectList<List<int>> GenresList { get; set; }

     [Display(Name = "Rating")]
     [BsControl(BsControlType.NumberInline)]
     public BsRangeItem<int?> Rating { get; set; }

     [Required]
     [Display(Name = "Recommended")]
     [BsControl(BsControlType.RadioButtonList)]
     public BsSelectList<YesNoValueTypes?> Recommended { get; set; }

     [Display(Name = "Poster Url", Prompt = "http://site.com/image.png")]
     [BsControl(BsControlType.Url)]
     public string Poster { get; set; }
 }
            

This model must represent the Movie model. As you can see, we have all the properties needed to create a new Movie Entity.

We annotate the properties with Display attributes, Required attributes and BsControl attributes, as needed. This way we ensure that the fields will display and behave correctly in the view.

We also need to include the MoviesNewModel inside the Toolbar constructor. To do this we edit the MoviesViewModel and we replace tha constructor that takes only the MoviesSearchModel with an overloaded constructor. This constructor will also accept the MoviesNewModel.

You can see how the modified MoviesViewModel should look like in the snippet below.

Modified Movies View Model

public class MoviesViewModel
{
    [BsGrid(HasDetails = true, Theme = BsTheme.Blue)]
    [Display(Name = "Top Movies")]
    public BsGridModel<MoviesRowModel> Grid { get; set; }

    [BsToolbar(Theme = BsTheme.Black)]
    [Display(Name = "Top Movies")]
    public BsToolbarModel<MoviesSearchModel, MoviesNewModel> Toolbar { get; set; }
}
            

Now we can create the _New partial view inside the Views/Home/Toolbar Folder right next to the _Toolbar view.

New Partial View

@using BForms.Html
@using BForms.Models
@using MyGrid.Mock
@model MyGrid.Models.MoviesNewModel
@using (Html.BsBeginForm())
{
    <h3>Add Form</h3>

    <div class="col-sm-6 col-lg-6 form-group @Html.BsValidationCssFor(m => m.Title)">
        @Html.BsLabelFor(m => m.Title)
        <div class="input-group">
            @Html.BsGlyphiconAddon(Glyphicon.FacetimeVideo)
            @Html.BsInputFor(m => m.Title)
            @Html.BsValidationFor(m => m.Title)
        </div>
    </div>

    <div class="col-sm-6 col-lg-4 form-group @Html.BsValidationCssFor(m => m.ReleaseDate)">
        @Html.BsLabelFor(m => m.ReleaseDate)
        <div class="input-group">
            @Html.BsGlyphiconAddon(Glyphicon.Calendar)
            @Html.BsInputFor(model => model.ReleaseDate)
            @Html.BsValidationFor(m => m.ReleaseDate)
        </div>
    </div>

    <div class="col-sm-6 col-lg-2 form-group @Html.BsValidationCssFor(m => m.Recommended)">
        @Html.BsLabelFor(m => m.Recommended)
        <div class="input-group">
            @Html.BsSelectFor(model => model.Recommended, new Dictionary<string, object>() { { "data-initialvalue", (byte)YesNoValueTypes.Yes } }, null)
            @Html.BsValidationFor(m => m.Recommended)
        </div>
    </div>
    
    <div class="col-sm-4 col-lg-4 form-group @Html.BsValidationCssFor(m => m.GrossRevenue)">
        @Html.BsLabelFor(m => m.GrossRevenue)
        <div class="input-group">
            @Html.BsGlyphiconAddon(Glyphicon.Usd)
            @Html.BsInputFor(m => m.GrossRevenue)
            @Html.BsValidationFor(m => m.GrossRevenue)
        </div>
    </div>
    
    <div class="col-sm-4 col-lg-4 form-group @Html.BsValidationCssFor(m => m.WeekendRevenue)">
        @Html.BsLabelFor(m => m.WeekendRevenue)
        <div class="input-group">
            @Html.BsGlyphiconAddon(Glyphicon.Usd)
            @Html.BsInputFor(m => m.WeekendRevenue)
            @Html.BsValidationFor(m => m.WeekendRevenue)
        </div>
    </div>
    
        <div class="col-sm-4 col-lg-4 form-group @Html.BsValidationCssFor(m => m.Rating)">
        @Html.BsLabelFor(m => m.Rating)
        <div class="input-group">
            @Html.BsGlyphiconAddon(Glyphicon.Star)
            @Html.BsInputFor(model => model.Rating)
            @Html.BsValidationFor(m => m.Rating)
        </div>
    </div>
    
    <div class="col-sm-12 col-lg-12 form-group @Html.BsValidationCssFor(m => m.Poster)">
        @Html.BsLabelFor(m => m.Poster)
        <div class="input-group">
            @Html.BsGlyphiconAddon(Glyphicon.Link)
            @Html.BsInputFor(m => m.Poster)
            @Html.BsValidationFor(m => m.Poster)
        </div>
    </div>
    
        <div class="col-sm-12 col-lg-12 form-group @Html.BsValidationCssFor(m => m.GenresList)">
        @Html.BsLabelFor(m => m.GenresList)
        <div class="input-group">
            @Html.BsGlyphiconAddon(Glyphicon.Tags)
            @Html.BsSelectFor(model => model.GenresList)
            @Html.BsValidationFor(m => m.GenresList)
        </div>
    </div>


    <div class="col-sm-12 col-md-12 col-lg-12 grid_toolbar_submit">
        <button type="submit" class="btn btn-default js-btn-save" data-action="@Url.Action("New")">Add</button>
        <a href="#" class="js-btn-reset">Reset</a>
    </div>
}
            

This view looks much like our editable Details views. The main difference is the Add button that has a data attribute set to point to the New Action in the Controller. We will create this action a little later.

The Add button is initialize automaticaly in javascript. All you need to do is to add the js-btn-save class.

Now that we have the _New partial view we can modify the _Toolbar view.

Modified Toolbar View

@model MyGrid.Models.MoviesViewModel
@using BForms.Grid
@using BForms.Html

@(Html.BsToolbarFor(x => x.Toolbar)
    .DisplayName("Top Movies")
    .ConfigureActions(ca =>
    {
        ca.Add(BsToolbarActionType.Add).Text("Add").Tab(x => Html.BsPartialPrefixed(y => y.New, "Toolbar/_New", x));
        
        ca.Add<BsToolbarQuickSearch>().Placeholder("Search");
        
        ca.Add(BsToolbarActionType.AdvancedSearch).Tab(x => Html.BsPartialPrefixed(y => y.Search, "Toolbar/_Search", x));
    })
)
            

We add the the _New button and form just like we did with the AdvancedSearch. The difference is that we use BsToolbarActionType.Add instead of BsToolbarActionType.AdvancedSearch. We also have to provide the _New partial view inside the Tab() helper.

This is how the toolbar should look like with the collapdes Add button:


We still have to modify the Controller to make it work.

The first step is to modify the Index() Action.

Modified Index Action

public ActionResult Index()
{
    ....

    var model = new MoviesViewModel
            {
                Grid = gridModel,
                Toolbar = new BsToolbarModel<MoviesSearchModel, MoviesNewModel>
                {
                    Search = _gridRepository.GetSearchForm(),
                    New = _gridRepository.GetNewForm()
                }
            };

    ....

    return View(model);
}


            

We modify the action to use the updated view model constructor BsToolbarModel<MoviesSearchModel, MoviesNewModel>

We also need to fill the Toolbar.New model by using New = _gridRepository.GetNewForm()

Next we will add the GetNewForm() function to the repository.

Get New Form Method

public MoviesNewModel GetNewForm()
{
    var genres = new BsSelectList<List<int>>();
    genres.ItemsFromEnum(typeof (MovieGenre));
    genres.SelectedValues = new List<int>();

    return new MoviesNewModel
    {
        ReleaseDate = new BsDateTime{ DateValue = DateTime.Now },
        GenresList = genres,
        Rating = new BsRangeItem<int?>
        {
            ItemValue = 9,
            MinValue = 0,
            MaxValue = 10,
            TextValue = "0-10",
            Display = "Movie Rating"
        }
    };
}
            

This repository method is used to fill the form controls with initial data. We only initialize the Genres Select List and the Release Date Picker.

Now we can visualize the uncollapsed New form inside the toolbar.


The last step is to add the New() Action to the controller. This Action is used when the form is submitted.

New Controller Action

public BsJsonResult New(BsToolbarModel<MoviesSearchModel, MoviesNewModel> model)
{
    var msg = string.Empty;
    var status = BsResponseStatus.Success;
    var row = string.Empty;

    try
    {
        if (ModelState.IsValid)
        {
            var rowModel = _gridRepository.Create(model.New);

            var viewModel = _gridRepository.ToBsGridViewModel(rowModel).Wrap<MoviesViewModel>(x => x.Grid);

            row = this.BsRenderPartialView("Grid/_Grid", viewModel);
        }
        else
        {
            return new BsJsonResult(
                new Dictionary<string, object> { { "Errors", ModelState.GetErrors() } },
                BsResponseStatus.ValidationError);
        }
    }
    catch (Exception ex)
    {
        msg = "Server Error";
        status = BsResponseStatus.ServerError;
    }

    return new BsJsonResult(new
    {
        Row = row
    }, status, msg);
}
            

This action gets the Toolbar model and calls the _gridRepository.Create(model.New) Method.

The Create() method will create the new entity and will return the row model for this entity. Then the grid will be updated with the newly added row.

Here is how the Create() method should look like in the repository:

Create Method Repository

public MoviesRowModel Create(MoviesNewModel model)
{
    var entity = new Movie();

    if(model != null)
    {
        entity.Id = db.Movies.Count() + 1;
        if(model.Recommended.SelectedValues.HasValue)
            entity.IsRecommended = model.Recommended.SelectedValues.Value == YesNoValueTypes.Yes ? true : false;
        entity.Title = model.Title;
        if (model.ReleaseDate.DateValue.HasValue)
            entity.ReleaseDate = model.ReleaseDate.DateValue.Value;
        if (!string.IsNullOrEmpty(model.Poster))
            entity.Poster = model.Poster;
        entity.GrossRevenue = model.GrossRevenue;
        entity.WeekendRevenue = model.WeekendRevenue;
        if (model.Rating.ItemValue.HasValue)
            entity.Rating = (double)model.Rating.ItemValue.Value;
        entity.Genres = string.Join(",", model.GenresList.SelectedValues);
    };

    db.Movies.Add(entity);
    db.SaveChanges();

    return MapMovie_MovieRowModel(entity);
}
            

After we create the new Movie Entity, we call db.Movies.Add(entity) and db.SaveChanges() to save the changes to the database.

At last we use MapMovie_MovieRowModel(entity) to map the entity to the row model, and we return it.

This is how the grid looks like with new rows added:


Now we have a fully functional grid and toolbar. We can add new rows, we can filter the grid with QuickSearch and AdvancedSearch, and we can edit existing rows.


MoviesTop - Grid and Toolbar Example Code


using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BForms.Models;
using BForms.Mvc;
using MyGrid.Mock;

namespace MyGrid.Models
{
    public class MoviesRowModel : BsGridRowModel<MovieDetailsModel>
    {
        public int Id { get; set; }

        [BsGridColumn(Width = 4, IsEditable = true, IsSortable = true)]
        public string Title { get; set; }

        [BsGridColumn(Width = 2, IsEditable = false, IsSortable = true)]
        public decimal WeekendRevenue { get; set; }

        [BsGridColumn(Width = 2, IsEditable = false, IsSortable = false)]
        public decimal GrossRevenue { get; set; }

        [BsGridColumn(Width = 2, IsEditable = false, IsSortable = true)]
        public DateTime ReleaseDate { get; set; }

        [BsGridColumn(Width = 2, IsEditable = false, IsSortable = true)]
        public bool Recommended { get; set; }

        public override object GetUniqueID()
        {
            return Id;
        }
    }

    public class MoviesViewModel
    {
        [BsGrid(HasDetails = true, Theme = BsTheme.Blue)]
        [Display(Name = "Top Movies")]
        public BsGridModel<MoviesRowModel> Grid { get; set; }

        [BsToolbar(Theme = BsTheme.Black)]
        [Display(Name = "Top Movies")]
        public BsToolbarModel<MoviesSearchModel, MoviesNewModel> Toolbar { get; set; }
    }

    public class MoviesSearchModel
    {
        public MoviesSearchModel()
        {
            Recommended = new BsSelectList<YesNoValueTypes?>();
            Recommended.ItemsFromEnum(typeof(YesNoValueTypes));
            Recommended.SelectedValues = YesNoValueTypes.Both;
        }

        [Display(Name = "Title")]
        [BsControl(BsControlType.TextBox)]
        public string Title { get; set; }

        [Display(Name = "Release Date Interval")]
        [BsControl(BsControlType.DatePickerRange)]
        public BsRange<DateTime?> ReleaseDate { get; set; }

        [BsControl(BsControlType.RadioButtonList)]
        [Display(Name = "Recommended")]
        public BsSelectList<YesNoValueTypes?> Recommended { get; set; }
    }

    public class MoviesNewModel
    {
        public MoviesNewModel()
        {
            Recommended = new BsSelectList<YesNoValueTypes?>();
            Recommended.ItemsFromEnum(typeof(YesNoValueTypes), YesNoValueTypes.Both);
            Recommended.SelectedValues = YesNoValueTypes.Yes; 
        }

        [Required]
        [Display(Name = "Movie Title")]
        [BsControl(BsControlType.TextBox)]
        public string Title { get; set; }

        [Display(Name = "Weekend Revenue")]
        [BsControl(BsControlType.TextBox)]
        public decimal WeekendRevenue { get; set; }

        [Display(Name = "Gross Revenue")]
        [BsControl(BsControlType.TextBox)]
        public decimal GrossRevenue { get; set; }

        [Required]
        [Display(Name = "Release Date")]
        [BsControl(BsControlType.DateTimePicker)]
        public BsDateTime ReleaseDate { get; set; }

        [Display(Name = "Genres", Prompt = "Select movie genres")]
        [BsControl(BsControlType.ListBox)]
        public BsSelectList<List<int>> GenresList { get; set; }

        [Display(Name = "Rating")]
        [BsControl(BsControlType.NumberInline)]
        public BsRangeItem<int?> Rating { get; set; }

        [Required]
        [Display(Name = "Recommended")]
        [BsControl(BsControlType.RadioButtonList)]
        public BsSelectList<YesNoValueTypes?> Recommended { get; set; }

        [Display(Name = "Poster Url", Prompt = "http://site.com/image.png")]
        [BsControl(BsControlType.Url)]
        public string Poster { get; set; }
    }

    public class MovieDetailsModel
    {
        public MovieDetailsModel()
        {
            IsRecommendedRadioButton = new BsSelectList<YesNoValueTypes>();
            IsRecommendedRadioButton.ItemsFromEnum(typeof(YesNoValueTypes), YesNoValueTypes.Both);
            IsRecommendedRadioButton.SelectedValues = YesNoValueTypes.Both;
        }

        public int Id { get; set; }

        [Required]
        [Display(Name = "Title")]
        [BsControl(BsControlType.TextBox)]
        public string Title { get; set; }

        [Required]
        [Display(Name = "Weekend Revenue")]
        [BsControl(BsControlType.Number)]
        public decimal WeekendRevenue { get; set; }

        [Display(Name = "Gross Revenue")]
        [BsControl(BsControlType.Number)]
        public decimal GrossRevenue { get; set; }

        [Required]
        [Display(Name = "Release Date")]
        [BsControl(BsControlType.DatePicker)]
        public BsDateTime ReleaseDate { get; set; }

        [Display(Name = "Genres")]
        [BsControl(BsControlType.ListBox)]
        public BsSelectList<List<int>> GenresList { get; set; }
        public string Genres { get; set; }

        [Display(Name = "Rating")]
        [BsControl(BsControlType.Number)]
        public double Rating { get; set; }

        [Required]
        [Display(Name = "Recomended")]
        [BsControl(BsControlType.RadioButtonList)]
        public BsSelectList<YesNoValueTypes> IsRecommendedRadioButton { get; set; }
        public bool Recommended { get; set; }

        [Display(Name = "Poster Image")]
        [BsControl(BsControlType.Url)]
        public string Poster { get; set; }
    }

    public enum EditComponents
    {
        Info = 1,
        Revenue = 2
    }
}
            
@model MyGrid.Models.MoviesViewModel
@using BForms.Html
@using BForms.Grid
@using BForms.Models
@using MyGrid.Models


@{
    var builder = new BsGridHtmlBuilder<MoviesViewModel, MoviesRowModel>();
}  

@(
Html.BsGridFor(m => m.Grid, builder)
.ConfigureColumns(cols =>
{
    cols.For(c => c.ReleaseDate)
        .Name("Released")
        .Text(x => String.Format("{0:MMM yyyy}", x.ReleaseDate));
    cols.For(c => c.WeekendRevenue)
        .Text(x => x.WeekendRevenue + " mil$");
    cols.For(c => c.GrossRevenue)
        .Text(x => x.GrossRevenue + " mil$");
    cols.For(c => c.Recommended)
        .Text(x => x.Recommended ? Html.BsGlyphicon(Glyphicon.ThumbsUp).ToHtmlString() : Html.BsGlyphicon(Glyphicon.ThumbsDown).ToHtmlString());
})
.ConfigureRows(cfg => cfg.DetailsTemplate(row => Html.Partial("Grid/Details/_Index", row.Details).ToString())
                         .HtmlAttributes(row => new Dictionary<string, object> { { "data-objid", row.Id }, { "data-active", row.Recommended } })
                         .Highlighter(row => row.Recommended ? "#59b444" : "#f0ad4e"))
.GridResetButton()
.ConfigureBulkActions(bulk =>
        {
            bulk.AddAction().StyleClass("btn-success js-btn-recommend_selected").Title("Recommend selected").GlyphIcon(Glyphicon.ThumbsUp);
            bulk.AddAction().StyleClass("btn-warning js-btn-unrecommend_selected").Title("Unrecommend selected").GlyphIcon(Glyphicon.ThumbsDown);
            bulk.AddAction(BsBulkActionType.Delete);

            bulk.AddSelector(BsBulkSelectorType.All);
            bulk.AddSelector().StyleClass("js-actives").Text("Recomended");
            bulk.AddSelector().StyleClass("js-inactives").Text("Unrecommended");
            bulk.AddSelector(BsBulkSelectorType.None);

            bulk.ForSelector(BsBulkSelectorType.All).Text("All");
            bulk.ForSelector(BsBulkSelectorType.None).Text("None");
        })
.PagerSettings(new BsPagerSettings
     {
        Size = 5,
        ShowFirstLastButtons = true,
        ShowPrevNextButtons = true,
        HasPagesText = true,
        HasPageSizeSelector = true
     })
)
            
@model MyGrid.Models.MoviesViewModel
@using BForms.Grid
@using BForms.Html

@(Html.BsToolbarFor(x => x.Toolbar)
    .DisplayName("Top Movies")
    .ConfigureActions(ca =>
    {
        ca.Add(BsToolbarActionType.Add).Text("Add").Tab(x => Html.BsPartialPrefixed(y => y.New, "Toolbar/_New", x));
        
        ca.Add<BsToolbarQuickSearch>().Placeholder("Search");
        
        ca.Add(BsToolbarActionType.AdvancedSearch).Tab(x => Html.BsPartialPrefixed(y => y.Search, "Toolbar/_Search", x));
    })
)
            
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using BForms.Grid;
using BForms.Models;
using BForms.Mvc;
using MyGrid.Models;
using MyGrid.Repositories;
using RequireJS;

namespace MyGrid.Controllers
{
    public class HomeController : BaseController
    {
        #region Properties and Constructor
        private readonly MoviesGridRepository _gridRepository;

        public HomeController()
        {
            _gridRepository = new MoviesGridRepository(Db);
        }
        #endregion

        public ActionResult Index()
        {
            var bsGridSettings = new BsGridBaseRepositorySettings
            {
                Page = 1,
                PageSize = 5
            };

            var savedSettings = GetGridSettings();

            if (savedSettings != null)
            {
                bsGridSettings.OrderableColumns = savedSettings.OrderableColumns;
                bsGridSettings.OrderColumns = savedSettings.OrderColumns;
                bsGridSettings.PageSize = savedSettings.PageSize;
            }

            var gridModel = _gridRepository.ToBsGridViewModel(bsGridSettings);

            var model = new MoviesViewModel
            {
                Grid = gridModel,
                Toolbar = new BsToolbarModel<MoviesSearchModel, MoviesNewModel>
                {
                    Search = _gridRepository.GetSearchForm(),
                    New = _gridRepository.GetNewForm()
                }
            };

            var options = new Dictionary<string, object>
            {
                {"pagerUrl", Url.Action("Pager")},
                {"getRowsUrl", Url.Action("GetRows")},
                {"recommendUnrecommendUrl", Url.Action("RecommendUnrecommend")},
                {"updateUrl", Url.Action("Update")},
                {"deleteUrl", Url.Action("Delete")},
                {"editComponents", RequireJsHtmlHelpers.ToJsonDictionary<EditComponents>()}
            };

            RequireJsOptions.Add("index", options);

            return View(model);
        }

        #region Ajax
        public BsJsonResult Pager(BsGridRepositorySettings<MoviesSearchModel> settings)
        {
            var msg = string.Empty;
            var status = BsResponseStatus.Success;
            var html = string.Empty;
            var count = 0;

            try
            {
                SaveGridSettings(settings);

                var viewModel = _gridRepository.ToBsGridViewModel(settings, out count).Wrap<MoviesViewModel>(x => x.Grid);

                html = this.BsRenderPartialView("Grid/_Grid", viewModel);
            }
            catch (Exception ex)
            {
                msg = ex.Message;
                status = BsResponseStatus.ServerError;
            }

            return new BsJsonResult(new
            {
                Count = count,
                Html = html
            }, status, msg);
        }

        public BsJsonResult GetRows(List<BsGridRowData<int>> items)
        {
            var msg = string.Empty;
            var status = BsResponseStatus.Success;
            var rowsHtml = string.Empty;

            try
            {
                rowsHtml = GetRowsHtml(items);
            }
            catch (Exception ex)
            {
                msg = "<strong>Server Error!</strong> " + ex.Message;
                status = BsResponseStatus.ServerError;
            }

            return new BsJsonResult(new
            {
                RowsHtml = rowsHtml
            }, status, msg);
        }

        public BsJsonResult New(BsToolbarModel<MoviesSearchModel, MoviesNewModel> model)
        {
            var msg = string.Empty;
            var status = BsResponseStatus.Success;
            var row = string.Empty;

            try
            {
                if (ModelState.IsValid)
                {
                    var rowModel = _gridRepository.Create(model.New);

                    var viewModel = _gridRepository.ToBsGridViewModel(rowModel).Wrap<MoviesViewModel>(x => x.Grid);

                    row = this.BsRenderPartialView("Grid/_Grid", viewModel);
                }
                else
                {
                    return new BsJsonResult(
                        new Dictionary<string, object> { { "Errors", ModelState.GetErrors() } },
                        BsResponseStatus.ValidationError);
                }
            }
            catch (Exception ex)
            {
                msg = "Server Error";
                status = BsResponseStatus.ServerError;
            }

            return new BsJsonResult(new
            {
                Row = row
            }, status, msg);
        }

        public BsJsonResult Delete(List<BsGridRowData<int>> items)
        {
            var msg = string.Empty;
            var status = BsResponseStatus.Success;

            try
            {
                foreach (var item in items)
                {
                    _gridRepository.Delete(item.Id);
                }
            }
            catch (Exception ex)
            {
                msg = "<strong>Server Error!</strong> " + ex.Message;
                status = BsResponseStatus.ServerError;
            }

            return new BsJsonResult(null, status, msg);
        }

        public BsJsonResult Update(MovieDetailsModel model, int objId, EditComponents componentId)
        {
            
            var msg = string.Empty;
            var status = BsResponseStatus.Success;
            var html = string.Empty;

            
            try
            {
                ClearModelState(ModelState, componentId);

                if (ModelState.IsValid)
                {
                    var detailsModel = _gridRepository.Update(model, objId, componentId);


                    switch (componentId)
                    {
                        case EditComponents.Info:
                            html = this.BsRenderPartialView("Grid/Details/_InfoReadonly", detailsModel);
                            break;
                        case EditComponents.Revenue:
                            html = this.BsRenderPartialView("Grid/Details/_RevenueReadonly", detailsModel);
                            break;
                    }

                    var rowModel = _gridRepository.ReadRow(objId);

                    var viewModel = _gridRepository.ToBsGridViewModel(rowModel, true).Wrap<MoviesViewModel>(x => x.Grid);

                    html = this.BsRenderPartialView("Grid/_Grid", viewModel);
                }
            }
            catch (Exception ex)
            {
                msg = "<strong>Server Error!</strong> " + ex.Message;
                status = BsResponseStatus.ServerError;
            }
            

            return new BsJsonResult(new
            {
                RowsHtml = html
            }, status, msg);
        }

        public BsJsonResult RecommendUnrecommend(List<BsGridRowData<int>> items, bool? recommended)
        {
            var msg = string.Empty;
            var status = BsResponseStatus.Success;
            var rowsHtml = string.Empty;

            try
            {
                foreach (var item in items)
                {
                    _gridRepository.RecommendUnrecommend(item.Id, recommended);
                }

                rowsHtml = GetRowsHtml(items);
            }
            catch (Exception ex)
            {
                msg = "<strong>Server Error!</strong> " + ex.Message;
                status = BsResponseStatus.ServerError;
            }

            return new BsJsonResult(new
            {
                RowsHtml = rowsHtml
            }, status, msg);
        }



        [NonAction]
        private string GetRowsHtml(List<BsGridRowData<int>> items)
        {
            var ids = items.Select(x => x.Id).ToList();
            var rowsModel = _gridRepository.ReadRows(ids);
            var viewModel = _gridRepository.ToBsGridViewModel(rowsModel, row => row.Id, items).Wrap<MoviesViewModel>(x => x.Grid);

            var savedSettings = GetGridSettings();

            if (savedSettings != null)
            {
                viewModel.Grid.BaseSettings.OrderableColumns = savedSettings.OrderableColumns;
                viewModel.Grid.BaseSettings.OrderColumns = savedSettings.OrderColumns;
                viewModel.Grid.BaseSettings.PageSize = savedSettings.PageSize;
            }
             
            return this.BsRenderPartialView("Grid/_Grid", viewModel);
        }

        [NonAction]
        public void ClearModelState(ModelStateDictionary ms, EditComponents componentId)
        {
            switch (componentId)
            {
                case EditComponents.Info:
                    ms.ClearModelState(new List<string>() { "Title", "Poster", "Rating", "GenresList" });
                    break;
                case EditComponents.Revenue:
                    ms.ClearModelState(new List<string>() { "GrossRevenue", "WeekendRevenue", "ReleaseDate" });
                    break;
            }
        }

        [NonAction]
        public void SaveGridSettings(BsGridRepositorySettings<MoviesSearchModel> settings)
        {
            if (settings.OrderColumns != null)
            {
                Session["GridSettings"] = new BsGridSavedSettings
                {
                    PageSize = settings.PageSize,
                    OrderableColumns = settings.OrderableColumns,
                    OrderColumns = settings.OrderColumns
                };
            }
        }

        [NonAction]
        public BsGridSavedSettings GetGridSettings()
        {
            return Session["GridSettings"] as BsGridSavedSettings;
        }

        [Serializable]
        public class BsGridSavedSettings
        {
            public int PageSize { get; set; }

            public List<BsColumnOrder> OrderableColumns { get; set; }

            public Dictionary<string, int> OrderColumns { get; set; }
        }

        #endregion

    }
}

            
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using BForms.Grid;
using MyGrid.Mock;
using MyGrid.Models;
using BForms.Models;
using Newtonsoft.Json;

namespace MyGrid.Repositories
{
    public class MoviesGridRepository : BsBaseGridRepository<Movie, MoviesRowModel>
    {
        #region Properties and Constructor
        private BFormsContext db;

        
        public BsGridRepositorySettings<MoviesSearchModel> Settings
        {
            get
            {
                return settings as BsGridRepositorySettings<MoviesSearchModel>;
            }
        }

        public MoviesGridRepository(BFormsContext _db)
        {
            db = _db;
        }
        #endregion

        #region Mappers
        public Func<Movie, MoviesRowModel> MapMovie_MovieRowModel = x =>
            new MoviesRowModel
            {
                Id = x.Id,
                Title = x.Title,
                WeekendRevenue = x.WeekendRevenue,
                GrossRevenue = x.GrossRevenue,
                ReleaseDate = x.ReleaseDate,
                Recommended = x.IsRecommended
            };

        public Func<Movie, MovieDetailsModel> MapMovie_MovieDetailsModel = x =>
            new MovieDetailsModel
            {
                Id = x.Id,
                Title = x.Title,
                GrossRevenue = x.GrossRevenue,
                WeekendRevenue = x.WeekendRevenue,
                Recommended = x.IsRecommended,
                Rating = x.Rating,
                Genres = x.Genres,
                Poster = x.Poster,
                ReleaseDate = new BsDateTime()
                {
                    DateValue = x.ReleaseDate
                }
            };
        #endregion

        #region Query/Order/Map
        public override IQueryable<Movie> Query()
        {
            var query = db.Movies.AsQueryable();

            return Filter(query);
        }

        public IQueryable<Movie> Filter(IQueryable<Movie> query)
        {

            if (this.Settings != null)
            {
                if (!string.IsNullOrEmpty(Settings.QuickSearch))
                {
                    var searched = Settings.QuickSearch.ToLower();

                    var queryQuick = query.Where(x => x.Title.ToLower().Contains(searched) ||
                                                        x.Genres.ToLower().Contains(searched));
                    query = queryQuick.Select(x => x);
                }
                else if (this.Settings.Search != null)
                {
                    #region Release Date

                    if (this.Settings.Search.ReleaseDate != null)
                    {
                        var fromDate = this.Settings.Search.ReleaseDate.From;
                        var toDate = this.Settings.Search.ReleaseDate.To;

                        if (fromDate.ItemValue.HasValue)
                        {
                            query = query.Where(x => x.ReleaseDate >= fromDate.ItemValue.Value);
                        }
                        if (toDate.ItemValue.HasValue)
                        {
                            query = query.Where(x => x.ReleaseDate <= toDate.ItemValue.Value);
                        }
                    }

                    #endregion

                    #region Title

                    if (!string.IsNullOrEmpty(this.Settings.Search.Title))
                    {
                        var title = this.Settings.Search.Title.ToLower();
                        query = query.Where(x => x.Title.ToLower().Contains(title));
                    }

                    #endregion

                    #region Recommended

                    if (this.Settings.Search.Recommended.SelectedValues.HasValue)
                    {
                        var isEnabled = this.Settings.Search.Recommended.SelectedValues.Value;

                        if (isEnabled == YesNoValueTypes.Yes)
                        {
                            query = query.Where(x => x.IsRecommended);
                        }
                        else if (isEnabled == YesNoValueTypes.No)
                        {
                            query = query.Where(x => !x.IsRecommended);
                        }
                    }

                    #endregion
                }
            }        

            return query;
        }

        public override IOrderedQueryable<Movie> OrderQuery(IQueryable<Movie> query)
        {
            this.orderedQueryBuilder.OrderFor(x => x.Recommended, y => y.IsRecommended);

            var ordered = this.orderedQueryBuilder.Order(query, x => x.OrderByDescending(y => y.WeekendRevenue));

            return ordered;
        }

        public override IEnumerable<MoviesRowModel> MapQuery(IQueryable<Movie> query)
        {
            return query.Select(MapMovie_MovieRowModel);
        }

        public override void FillDetails(MoviesRowModel row)
        {
            row.Details = db.Movies.Where(x => x.Id == row.Id).Select(MapMovie_MovieDetailsModel).FirstOrDefault();

            if (row.Details != null)
            {
                FillDetailsProperties(row.Details);
            }
        }

        #endregion

        #region CRUD
        public MoviesRowModel ReadRow(int objId)
        {
            return db.Movies.Where(x => x.Id == objId).Select(MapMovie_MovieRowModel).FirstOrDefault();
        }

        public MoviesRowModel Create(MoviesNewModel model)
        {
            var entity = new Movie();

            if(model != null)
            {
                entity.Id = db.Movies.Count() + 1;
                if(model.Recommended.SelectedValues.HasValue)
                    entity.IsRecommended = model.Recommended.SelectedValues.Value == YesNoValueTypes.Yes ? true : false;
                entity.Title = model.Title;
                if (model.ReleaseDate.DateValue.HasValue)
                    entity.ReleaseDate = model.ReleaseDate.DateValue.Value;
                if (!string.IsNullOrEmpty(model.Poster))
                    entity.Poster = model.Poster;
                entity.GrossRevenue = model.GrossRevenue;
                entity.WeekendRevenue = model.WeekendRevenue;
                if (model.Rating.ItemValue.HasValue)
                    entity.Rating = (double)model.Rating.ItemValue.Value;
                entity.Genres = string.Join(",", model.GenresList.SelectedValues);
            };

            db.Movies.Add(entity);
            db.SaveChanges();

            return MapMovie_MovieRowModel(entity);
        }

        public MovieDetailsModel Update(MovieDetailsModel model, int objId, EditComponents componentId)
        {
            var entity = db.Movies.FirstOrDefault(x => x.Id == objId);

            if (entity != null)
            {
                switch (componentId)
                {
                    case EditComponents.Info:
                        entity.Title = model.Title;
                        entity.IsRecommended = model.IsRecommendedRadioButton.SelectedValues == YesNoValueTypes.Yes ? true : false;
                        if(!string.IsNullOrEmpty(model.Poster))
                            entity.Poster = model.Poster;
                        entity.Rating = model.Rating;
                        entity.Genres = string.Join(",",model.GenresList.SelectedValues);
                        break;
                    case EditComponents.Revenue:
                        entity.GrossRevenue = model.GrossRevenue;
                        entity.WeekendRevenue = model.WeekendRevenue;
                        if (model.ReleaseDate.DateValue.HasValue)
                            entity.ReleaseDate = model.ReleaseDate.DateValue.Value;
                        break;
                }
                db.SaveChanges();
            }

            return FillDetailsProperties(MapMovie_MovieDetailsModel(entity));
        }

        public List<MoviesRowModel> ReadRows(List<int> objIds)
        {
            return db.Movies.Where(x => objIds.Contains(x.Id)).Select(MapMovie_MovieRowModel).ToList();
        }

        public void RecommendUnrecommend(int objId, bool? recommended)
        {
            var entity = db.Movies.FirstOrDefault(x => x.Id == objId);

            if (entity != null)
            {
                entity.IsRecommended = recommended.HasValue ? recommended.Value : !entity.IsRecommended;
                db.SaveChanges();
            }
        }

        public void Delete(int objId)
        {
            var entity = db.Movies.FirstOrDefault(x => x.Id == objId);

            if (entity != null)
            {
                db.Movies.Remove(entity);
                db.SaveChanges();
            }
        }
        #endregion

        #region Helpers
        public MovieDetailsModel FillDetailsProperties(MovieDetailsModel detailsModel)
        {
            detailsModel.IsRecommendedRadioButton.SelectedValues = detailsModel.Recommended ? YesNoValueTypes.Yes : YesNoValueTypes.No;
            detailsModel.GenresList = new BsSelectList<List<int>>();
            detailsModel.GenresList.ItemsFromEnum(typeof(MovieGenre));
            detailsModel.GenresList.SelectedValues = new List<int>();
            var options = detailsModel.Genres.Split(',').ToList();
            foreach (string option in options)
            {
                MovieGenre genre;
                if (Enum.TryParse(option, true, out genre))
                    detailsModel.GenresList.SelectedValues.Add((int)genre);
            }

            return detailsModel;
        }

        public MoviesSearchModel GetSearchForm()
        {
            return new MoviesSearchModel
            {
                ReleaseDate = new BsRange<DateTime?>()
                {
                    From = new BsRangeItem<DateTime?>
                    {
                        ItemValue = new DateTime(2013, 1, 1)
                    },
                    To = new BsRangeItem<DateTime?>
                    {
                        ItemValue = DateTime.Now
                    }
                }
            };
        }

        public MoviesNewModel GetNewForm()
        {
            var genres = new BsSelectList<List<int>>();
            genres.ItemsFromEnum(typeof (MovieGenre));
            genres.SelectedValues = new List<int>();

            return new MoviesNewModel
            {
                ReleaseDate = new BsDateTime{ DateValue = DateTime.Now },
                GenresList = genres,
                Rating = new BsRangeItem<int?>
                {
                    ItemValue = 9,
                    MinValue = 0,
                    MaxValue = 10,
                    TextValue = "0-10",
                    Display = "Movie Rating"
                }
            };
        }

        #endregion
    }
}
            
require([
        'jquery',
        'bforms-namespace',
        'bforms-grid',
        'bforms-toolbar',
        'bootstrap',
        'bforms-ajax'
], function () {
    //#region Constructor and Properties
    var homeIndex = function (options) {
        this.options = $.extend(true, {}, options);
        this.init();
    };
    
    
    homeIndex.prototype.init = function () {
        this.$grid = $('#grid');
        this.$toolbar = $('#toolbar');
        
        this.initGrid();
        this.initToolbar();
    };
    //#endregion
    
    //#region Grid
    homeIndex.prototype.initGrid = function() {
        this.$grid.bsGrid({
            $toolbar: this.$toolbar,
            uniqueName: 'moviesGrid',
            pagerUrl: this.options.pagerUrl,
            detailsUrl: this.options.getRowsUrl,
            beforeRowDetailsSuccess: $.proxy(this._beforeDetailsSuccessHandler, this),
            afterRowDetailsSuccess: $.proxy(this._afterDetailsSuccessHandler, this),
            rowActions: [{
                    btnSelector: '.js-btn_state',
                    url: this.options.recommendUnrecommendUrl,
                    handler: $.proxy(this._recommendUnrecommendHandler, this),
                }, {
                    btnSelector: '.js-btn_delete',
                    url: this.options.deleteUrl,
                    init: $.proxy(this._deleteHandler, this),
                    context: this
                }],
            //#region filterButtons
            filterButtons: [{
                btnSelector: '.js-actives',
                filter: function ($el) {
                    return $el.data('active') == 'True';
                }
            }, {
                btnSelector: '.js-inactives',
                filter: function ($el) {
                    return $el.data('active') != 'True';
                },
            }],
            //#endregion
            
            //#region gridActions
            gridActions: [{
                btnSelector: '.js-btn-recommend_selected',
                handler: $.proxy(function ($rows, context) {
                    var data = {};

                    var items = context.getSelectedRows();

                    data.items = items;
                    data.recommended = true;

                    this._ajaxRecommendUnrecommend($rows, data, this.options.recommendUnrecommendUrl, function (response) {

                        context.updateRows(response.RowsHtml);

                    }, function (response) {
                        context._pagerAjaxError(response);
                    });
                }, this)
            }, {
                btnSelector: '.js-btn-unrecommend_selected',
                handler: $.proxy(function ($rows, context) {
                    var data = {};

                    var items = context.getSelectedRows();
                    data.items = items;
                    data.recommended = false;

                    this._ajaxRecommendUnrecommend($rows, data, this.options.recommendUnrecommendUrl, function (response) {

                        context.updateRows(response.RowsHtml);

                    }, function (response) {
                        context._pagerAjaxError(response);
                    });
                }, this)
            }, {
                btnSelector: '.js-btn-delete_selected',
                handler: $.proxy(function ($rows, context) {

                    var items = context.getSelectedRows();

                    this._ajaxDelete($rows, items, this.options.deleteUrl, $.proxy(function () {
                        $rows.remove();
                        context._evOnRowCheckChange($rows);
                        if (this.$grid.find('.grid_row[data-objid]').length == 0) {
                            this.$grid.bsGrid('refresh');
                        }
                    }, this), function (response) {
                        context._pagerAjaxError(response);
                    });
                }, this),
                popover: true
            }]
            //#endregion
        });
    };
    //#endregion
    
    //#region Toolbar
    homeIndex.prototype.initToolbar = function () {
        this.$toolbar.bsToolbar({
            uniqueName: 'moviesToolbar',
            subscribers: [this.$grid]
        });
    };
    //#endregion

    //#region DetailsHandler
    homeIndex.prototype._beforeDetailsSuccessHandler = function (e, data) {
        var $row = data.$row,
            response = data.data;

        var infoOpt = this._editableOptions($row, this.options.editComponents.Info);
        $row.find('.js-editableInfo').bsEditable(infoOpt);

        var revenueOpt = this._editableOptions($row, this.options.editComponents.Revenue);
        $row.find('.js-editableRevenue').bsEditable(revenueOpt);
    };

    homeIndex.prototype._editableOptions = function ($row, componentId) {
        return $.extend(true, {}, {
            url: this.options.updateUrl,
            prefix: 'x' + $row.data('objid') + '.',
            additionalData: {
                objId: $row.data('objid'),
                componentId: componentId
            },
            editSuccessHandler: $.proxy(function (editResponse) {
                this.$grid.bsGrid('updateRows', editResponse.RowsHtml);
            }, this)
        });
    };

    homeIndex.prototype._afterDetailsSuccessHandler = function (e, data) {
        var $row = data.$row;

        $row.find('.js-editableInfo').bsEditable('initValidation');
        $row.find('.js-editableRevenue').bsEditable('initValidation');
    };
    //#endregion
    
    //#region RecommendUnrecommendHandler
    homeIndex.prototype._recommendUnrecommendHandler = function (e, options, $row, context) {

        var data = [];

        data.push({
            Id: $row.data('objid'),
            GetDetails: $row.hasClass('open')
        });

        this._ajaxRecommendUnrecommend($row, data, options.url, function (response) {

            context.updateRows(response.RowsHtml);

        }, function (response) {
            context._rowActionAjaxError(response, $row);
        });

    };

    homeIndex.prototype._ajaxRecommendUnrecommend = function ($html, data, url, success, error) {
        var ajaxOptions = {
            name: '|recommendUnrecommend|' + $html.data('objid'),
            url: url,
            data: data,
            context: this,
            success: success,
            error: error,
            loadingElement: $html,
            loadingClass: 'loading'
        };
        $.bforms.ajax(ajaxOptions);
    };
    //#endregion
    
    //#region DeleteHandler
    homeIndex.prototype._deleteHandler = function (options, $row, context) {

        //add popover widget
        var $me = $row.find(options.btnSelector);
        $me.popover({
            html: true,
            placement: 'left',
            content: $('.popover-content').html()
        });

        // add delegates to popover buttons
        var tip = $me.data('bs.popover').tip();
        tip.on('click', '.bs-confirm', $.proxy(function (e) {
            e.preventDefault();

            var data = [];
            data.push({
                Id: $row.data('objid')
            });

            this._ajaxDelete($row, data, options.url, function () {
                $row.remove();
            }, function (response) {
                context._rowActionAjaxError(response, $row);
            });

            $me.popover('hide');
        }, this));
        tip.on('click', '.bs-cancel', function (e) {
            e.preventDefault();
            $me.popover('hide');
        });
    };

    homeIndex.prototype._ajaxDelete = function ($html, data, url, success, error) {
        var ajaxOptions = {
            name: '|delete|' + data,
            url: url,
            data: data,
            context: this,
            success: success,
            error: error,
            loadingElement: $html,
            loadingClass: 'loading'
        };
        $.bforms.ajax(ajaxOptions);
    };
    //#endregion

    //#region Dom Ready
    $(document).ready(function () {
        var page = new homeIndex(window.requireConfig.pageOptions.index);
    });
    //#endregion
});