Search:

Search all manuals
Search this manual
Manual
Couchbase Client Library: .NET (C#) 1.2
Community Wiki and Resources
Wiki: .NET Client Library
Download Client Library
.NET Client Library
Couchbase Developer Guide 2.0
Couchbase Server Manual 2.0
SDK Forum
Additional Resources
Community Wiki
Community Forums
Couchbase SDKs
Parent Section
2 Couchbase and ASP.NET MVC
Chapter Sections
Chapters

2.10. Paging

The final feature to implement on the brewery CRUD forms is paging.  It's important to state up front that paging in Couchbase does not work like paging in a typical RDBMS.  Though views have skip and limit filters that could be used create the standard paging experience, it's not advisable to take this approach. 

The skip filter still results in a read of index data starting with the first row of the index.  For example, if an index has 5000 rows and skip is set to 500 and limit is set to 50, 500 records are read and 50 returned.  Instead, linked-list style pagination is the recommended approach.  Paging should also consider the document ids because keys may collide.  However, in the breweries example, paging on name is safe because name is the source of the unique key.

First add an HTML footer to the list table in the Index view, right before the final closing table tag.  There is a link back to the first page and links to the previous and next pages.  A default page size of 10 is also used.  Each time the page is rendered, it sets the previous key to the start key of the previous page.  The next key will be explained shortly.

<tr>
    <td colspan="4">
        @Html.ActionLink("List", "Index", new { pagesize = 10 })
        @Html.ActionLink("< Previous", "Index", new { startKey = Request["previousKey"], pagesize = Request["pagesize"] ?? "10" })
        @Html.ActionLink("Next >", "Index", new { startKey = ViewBag.NextStartKey, previousKey =  ViewBag.StartKey, pagesize = Request["pagesize"] ?? "10"})
    </td>
</tr>

Modify the GetAllByName method in BreweryRepository to be able to handle range queries (startkey, endkey).

public IEnumerable<Brewery> GetAllByName(string startKey = null, string endKey = null, int limit = 0, bool allowStale = false)
{
    var view = GetView("by_name");
    if (limit > 0) view.Limit(limit);
    if (! allowStale) view.Stale(StaleMode.False);
    if (! string.IsNullOrEmpty(startKey)) view.StartKey(startKey);
    if (! string.IsNullOrEmpty(endKey)) view.StartKey(endKey);
    return view;
}

For the actual paging, modify the BreweryController's Index method to keep track of pages. The trick is to select page size + 1 from the view. The last element is not rendered, but its key is used as the start key of the next page. In simpler terms, the start key of the current page is the next page's previous key. The last element's key is not displayed, but is used as the next page's start key.

public ActionResult Index(string startKey, string nextKey, int pageSize = 25)
{
    var breweries = BreweryRepository.GetAllByName(startKey: startKey, limit: pageSize+1);
    ViewBag.StartKey = breweries.ElementAt(0).Name;
    ViewBag.NextStartKey = breweries.ElementAt(breweries.Count()-1).Name;
    return View(breweries.Take(pageSize));
}

Figure 2.14. Figure 14, Paging

Paging

At this point, breweries may be created, detailed (with Children), listed, updated and deleted.  The next step is to look at the brewery data from a different perspective, namely location.

Brewery documents have multiple properties related to their location.  There are state and city properties, as well as detailed geospatial data.  The first question to ask of the data is how many breweries exist for a given country.  Then within each country, the counts can be refined to see how many breweries are in a given state, then city and finally zip code.  All of these questions will be answered by the same view.

Create a view named “by_country” with the code below.  This view will not consider documents that don’t have all location properties.  The reason for this restriction is so that counts are accurate as you drill into the data.

function (doc, meta) { 
  if (doc.country && doc.state && doc.city && doc.code) {
 	emit([doc.country, doc.state, doc.city, doc.code], null);
  }
}

For this view, you’ll also want a reduce function, which will count the number of rows for a particular grouping by counting how many rows appear for that grouping.  So for example, when the group_level parameter is set to 2 brewery counts will be returned by city and state.  For an analogy, think of a SQL statement selecting a COUNT(*) and having a GROUP BY clause with city and state columns.

Couchbase has three built in reduce functions - _count, _sum and _stats.  For this view, _count and _sum will perform the same duties.  Emitting a 1 as a value means that _sum would sum the 1s for a grouping.  _count would simply count 1 for each row, even with a null value.

If you are using Model Views, then simply add CouchbaseViewKeyCount attributes to each of the properties that should be produced in the view.

[CouchbaseViewKeyCount("by_country", "country", 0, null)]
public string Country { get; set; }

[CouchbaseViewKeyCount("by_country", "state", 1)]
public string State { get; set; }

[CouchbaseViewKeyCount("by_country", "city", 2)]
public string City { get; set; }

[CouchbaseViewKeyCount("by_country", "code", 3)]
public string Code { get; set; }

This view demonstrates how to create ordered, composite keys from domain object properties using the Model Views framework.

The next step is to modify the BreweryRepository to include methods that will return aggregated results grouped at the appropriate levels.  This new method will return key value pairs where the key is the lowest grouped part of the key and the value is the count.  Also add an enum for group levels.

public IEnumerable<KeyValuePair<string, int>> GetGroupedByLocation(BreweryGroupLevels groupLevel, string[] keys = null)
{
    var view = GetViewRaw("by_country")
                .Group(true)
                .GroupAt((int)groupLevel);
    if (keys != null)
    {
        view.StartKey(keys);
        view.EndKey(keys.Concat(new string[] { "\uefff" }));
    }
    foreach (var item in view)
    {
        var key = item.ViewKey[(int)groupLevel-1].ToString();
        var value = Convert.ToInt32(item.Info["value"]);
        yield return new KeyValuePair<string, int>(key, value);
    }
}

Create a new controller named "CountriesController" to contain the actions for the new grouped queries. Use the empty controller template.

Figure 2.15. Figure 15, Add CountriesController

Add CountriesController

Modify the new controller to include the code below, which sets up the BreweryRepositoryReference and loads sends the view results to the MVC View.

public class CountriesController : Controller
{
    public BreweryRepository BreweryRepository { get; set; }
    public CountriesController()
    {
        BreweryRepository = new BreweryRepository();
    }
    public ActionResult Index()
    {
        var grouped = BreweryRepository.GetGroupedByLocation(BreweryGroupLevels.Country);
        return View(grouped);
    }
}

Next create a new directory under “Views” named “Countries.” Add a view named “Index” that is not strongly typed.

Figure 2.16. Figure 16, Add Countries Index view

Add Countries Index View

To the new view, add the Razor code below, which will simply display the keys and values as a list. It also links to the Provinces action, which you’ll create next.

@model dynamic
<h2>Brewery counts by country</h2>
<ul>
@foreach (KeyValuePair<string, int> item in Model)
{
    <li>
        @Html.ActionLink(item.Key, "Provinces", new { country = item.Key})
        (@item.Value)
    </li>
}
</ul>

Build and run your application and you should see a page like below.

Figure 2.17. Figure 17, Brewery counts by country

Brewery counts by country

Next, add the Provinces action to the CountriesController. This action will reuse the repository method, but will change the group level to Province (2) and pass the selected country to be used as a key to limit the query results.

public ActionResult Provinces(string country)
{
    var grouped = BreweryRepository.GetGroupedByLocation(
                BreweryGroupLevels.Province, new string[] { country } );
    return View(grouped);
}

Create another empty view named “Provinces” in the “Countries” directory under the “Views” directory. Include the content below, which is similar to the index content.

@model dynamic
<h2>Brewery counts by province in @Request["country"]</h2>
<ul>
@foreach (KeyValuePair<string, int> item in Model)
{
    <li>
        @Html.ActionLink(item.Key, "Cities",
            new { country = Request["country"], province = item.Key})
        (@item.Value)
    </li>
}
</ul>

Compile and run the app. You should see the Provinces page below.

Figure 2.18. Figure 18, Brewery counts by province

Brewery counts by province

Creating the actions and views for cities and codes is a similar process. Modify CountriesController to include new action methods as shown below.

public ActionResult Cities(string country, string province)
{
    var grouped = BreweryRepository.GetGroupedByLocation(
                BreweryGroupLevels.City, new string[] { country, province });
    return View(grouped);
}
public ActionResult Codes(string country, string province, string city)
{
    var grouped = BreweryRepository.GetGroupedByLocation(
                BreweryGroupLevels.PostalCode, new string[] { country, province, city });
    return View(grouped);
}

Then add a view named "Cities" with the Razor code below.

@model dynamic
<h2>Breweries counts by city in @Request["province"], @Request["country"]</h2>
<ul>
@foreach (KeyValuePair<string, int> item in Model)
{
    <li>
        @Html.ActionLink(item.Key, "Codes",
            new { country = Request["country"], 
                  province = Request["province"], 
                  city = item.Key})
        (@item.Value)
    </li>
}
</ul>

Then add a view named "Codes" with the Razor code below.

@model dynamic
<h2>Brewery counts by postal code in @Request["city"], @Request["province"], @Request["country"]</h2>
<ul>
@foreach (KeyValuePair<string, int> item in Model)
{
    <li>
       @Html.ActionLink(item.Key, "Details",
            new { country = Request["country"], 
                  province = Request["province"], 
                  city = Request["city"],
                  code = item.Key})
        (@item.Value)        
    </li>
}
</ul>
@Html.ActionLink("Back to Country List", "Index")

Compile and run the app. Navigate through the country and province listings to the cities listing. You should see the page below.

Figure 2.19. Figure 19, Brewery counts by city

Brewery counts by cities

Click through to the codes page and you should see the page below.

Figure 2.20. Figure 20, Brewery counts by poastal code

Brewery counts by postal code

The last step for this feature is to display the list of breweries for a given zip code. To implement this page, you need to add a new method to BreweryRepository named GetByLocation. This method will use the same view that we’ve been using, except it won’t execute the reduce step. Not executing the reduce step means that the results come back ungrouped and individual items are returned.

public IEnumerable<Brewery> GetByLocation(string country, string province, string city, string code)
{
    return GetView("by_country").Key(new string[] { country, province, city, code }).Reduce(false);
}

Then add a Details action method to the BreweriesController that calls this method and returns its results to the view.

public ActionResult Details(string country, string province, string city, string code)
{
    var breweries = BreweryRepository.GetByLocation(country, province, city, code);
    return View(breweries);
}

Create a Details view in the “Countries” folder with the Razor code below.

@model IEnumerable<CouchbaseBeersWeb.Models.Brewery>
<h2>Breweries in @Request["code"], @Request["city"], @Request["province"], @Request["country"]</h2>
<ul>
@foreach (var item in Model)
{
    <li>
        @Html.ActionLink(item.Name, "Details", "Breweries", new { id = item.Id }, new { })
    </li>
}
</ul>
@Html.ActionLink("Back to Country List", "Index")

Compile and run the app. Click through country, province and state on to the Codes view. The code above already has a link to this new Details page. When you click on a postal code, you should see a list of breweries as below.

Figure 2.21. Figure 21, Breweries by poastal code

Breweries by poastal code