Entity Framework Recipe: Alphabetical Paging In ASP.NET MVC

4.62 (13 votes)

This article looks at using the alphabet for paging data in MVC 5 with Entity Framework 6. Where it differs from most existing articles on alphabetical paging is that this article looks at generating paging links from the data instead of the alphabet. It also features a Bootstrap-friendly helper for generating the paging links.

Generating paging links directly from the alphabet is not necessarily a bad thing, but if there is no data to display against certain letters, your users will find themselves clicking links for no reason. You wouldn't render numerical paging links that yield no results, so why do it with the alphabet? I believe that it is better from a usability point of view to either refrain from displaying links that generate no results or to display them but make them inactive. Either way, you need to query the data to determine which links have matching results.This example uses the Northwind database, specifically data from the Products table. The default Northwind database contains no products beginning with the letters D, H, X or Y. Therefore it will serve admirably to illustrate the point about managing paging links that return no data.

The view needs a ViewModel:

public class AlphabeticalPagingViewModel
{
    public List<string> ProductNames { get; set; }
    public List<string> Alphabet
    {
        get
        {
            var alphabet = Enumerable.Range(65, 26).Select(i => ((char)i).ToString()).ToList();
            alphabet.Insert(0, "All");
            alphabet.Insert(1, "0-9");
            return alphabet;
        }
    }
    public List<string> FirstLetters { get; set; }
    public string SelectedLetter { get; set; }
    public bool NamesStartWithNumbers
    {
        get
        {
            var numbers = Enumerable.Range(0, 10).Select(i => i.ToString());
            return FirstLetters.Intersect(numbers).Any();
        }
    }
}

There's a List<string> for the product names to be displayed. The Alphabet property is populated with all the letters of the alphabet and has two further values prepended to the List - one to represent an "All" option and another to represent names beginning with numbers. The FirstLetter property holds the first letters/characters of all the existing products. The SelectedLetter property will be used to represent the value of the paging link that's clicked. The final property is a bool which will return true if any of the FirstLetters values are numbers. In this example, none of the product names begin with numbers, but if the example included e.g. company names like 3M, the property value would be true.

The model is instantiated in the Index action:

public ActionResult Index(string selectedLetter)
{
    var model = new AlphabeticalPagingViewModel {  SelectedLetter = selectedLetter };
    using (var context = new EFRecipeContext())
    {
        model.FirstLetters = context.Products
            .GroupBy(p => p.ProductName.Substring(0, 1))
            .Select(x => x.Key.ToUpper())
            .ToList();

        if (string.IsNullOrEmpty(selectedLetter) || selectedLetter == "All")
        {
            model.ProductNames = context.Products
                .Select(p => p.ProductName)
                .ToList();
        }
        else
        {
            if (selectedLetter == "0-9")
            {
                var numbers = Enumerable.Range(0, 10).Select(i => i.ToString());
                model.ProductNames = context.Products
                    .Where(p => numbers.Contains(p.ProductName.Substring(0, 1)))
                    .Select(p => p.ProductName)
                    .ToList();
            }
            else
            {
                model.ProductNames = context.Products
                    .Where(p => p.ProductName.StartsWith(selectedLetter))
                    .Select(p => p.ProductName)
                    .ToList();
            }
        }
    }
    return View(model);
}

The link that was clicked is fed into the method as a parameter - the selectedLetter. It's value is used to populate its respective property in the newly instantiated ViewModel. The first letters of all existing products are obtained from the database using a LINQ query. Instinct might lead you to try using a query that includes the Distinct() extension method, but in fact, GroupBy is the best operator for this type of query. Data is grouped by the first letter of each ProductName, which then becomes the Key for each group. That value is then projected into the List<string> that forms the FirstLetter property of the ViewModel.

The SelectedLetter value is then examined to establish what data to return. If it is empty or its value is "All", unfiltered data is returned. If the selectedLetter value is "0-9", all data that begins with any character between 0 and 9 is returned. Otherwise only data that starts with the selected letter is returned.

The next section of code shows how this data can be displayed in the view:

<ul class="pagination alpha">
    @foreach (var link in Model.Alphabet)
    {
        if (Model.FirstLetters.Contains(link) || (Model.NamesStartWithNumbers && link == "0-9") || link == "All")
        {
            if (Model.SelectedLetter == link || Model.SelectedLetter.IsEmpty() && link == "All")
            {
                <li class="active"><span>@link</span></li>
            }
            else
            {
                <li>@Html.ActionLink(link, "Index", new {selectedLetter = link})</li>
            }
        }
        else
        {
            <li class="inactive"><span>@link</span></li>
        }
    }
</ul>
<ul>
    @foreach (var item in Model.ProductNames)
    {
        <li>@item</li>
    }
</ul>

I have chosen to render the complete alphabet, but only to make letters that have matching data active links. So this code loops through all the entries in the Alphabet collection and checks each in turn. If the current letter of the Alphabet collection is also in the FirstLetters collection, or the current letter is "0-9" and there are entries that start with numbers, or the current letter is the "All" option, then a link should be rendered. Otherwise the list item is given a class of inactive and the letter is rendered in a span instead of an anchor element. If the current letter is the same as the SelectedLetter value, or there is no selected letter and the letter is "All", the link is given a class of active, representing the current page of data. Finally, the product names are rendered to the browser.

MVC5 With EF6 Alphabetical Paging

 

The design of the paging links is controlled by simply applying the pagination class to the ul element that contains the list items and including Bootstrap.

Html Helper

If you want to add alphabetical paging to other pages in your site, or if you simply prefer not to have so much looping in your views, you can create an Html Helper to generate the paging links. Here's one I made earlier:

public static class HtmlHelpers
{
    public static HtmlString AlphabeticalPager(this HtmlHelper html, string selectedLetter, IEnumerable<string> firstLetters, Func<string, string> pageLink)
    {
        var sb = new StringBuilder();
        var numbers = Enumerable.Range(0, 10).Select(i => i.ToString());
        var alphabet = Enumerable.Range(65, 26).Select(i => ((char)i).ToString()).ToList();
        alphabet.Insert(0, "All");
        alphabet.Insert(1, "0-9");

        var ul = new TagBuilder("ul");
        ul.AddCssClass("pagination");
        ul.AddCssClass("alpha");

        foreach (var letter in alphabet)
        {
            var li = new TagBuilder("li");
            if (firstLetters.Contains(letter) || (firstLetters.Intersect(numbers).Any() && letter == "0-9") || letter == "All")
            {
                if (selectedLetter == letter || selectedLetter.IsEmpty() && letter == "All")
                {
                    li.AddCssClass("active");
                    var span = new TagBuilder("span");
                    span.SetInnerText(letter);
                    li.InnerHtml = span.ToString();
                }
                else
                {
                    var a = new TagBuilder("a");
                    a.MergeAttribute("href", pageLink(letter));
                    a.InnerHtml = letter;
                    li.InnerHtml = a.ToString();
                }
            }
            else
            {
                li.AddCssClass("inactive");
                var span = new TagBuilder("span");
                span.SetInnerText(letter);
                li.InnerHtml = span.ToString();
            }
            sb.Append(li.ToString()); 
        }
        ul.InnerHtml = sb.ToString();
        return new HtmlString(ul.ToString());
    }
}

The helper moves the construction of the alphabet away from the ViewModel and into the body of the method. It also removes the need for the NamesStartWithNumber property, so the revised ViewModel is slimmer:

public class AlphabeticalPagingViewModel
{
    public List<string> ProductNames { get; set; }
    public List<string> FirstLetters { get; set; }
    public string SelectedLetter { get; set; }
}

The view itself is also a lot slimmer:

@Html.AlphabeticalPager(Model.SelectedLetter, Model.FirstLetters, x => Url.Action("Index", new { selectedLetter = x }))
<ul>
    @foreach (var item in Model.ProductNames)
    {
        <li>@item</li>
    }
</ul>

This article has shown how to use the LINQ GroupBy operator to get the first letters of existing data so that the generation of alphabetical paging links can be controlled to only include those that will yield results. In addition, the article presented a reusable Html Helper that takes care of generating the paging links while keeping both ViewModel and View uncluttered.

You might also like...

Date Posted:
Last Updated:
Posted by:
Total Views to date: 26833

17 Comments

- Satyabrata Mohapatra

Nice tutorial.Learned a lot.

- Mohammad

Very nice to see vvNext articles coming out already.Nice article!
Hope to see more

- Mike

@Mohammad,

Ermm... this article isn't about vNext.

- Fritz Mack

Very nice tutorial! Please can you send me the CSS for this article. Thank you!!!

- Mike

@Fritz,

The article makes use of Bootstrap (http://getbootstrap.com/)

- chandradev

Excellent article. Thanks for sharing this article.

- Isbael

Would you please share the code?

- Mike

@Isbael

The code is in the article. You can copy and paste it.

- donramon

Congratulations on your new website look and the excellent articles. Thank you!

- donramon

Now that WebMatrix is being deprecated, it would be great to see the WebMatrix book samples and articles ported
to Visual Studio, especially the ones with the jquery dialogs. Thanks.

- Mike

@donramon

You can open the Books samples in Visual Studio and work with them as they are. The code won't change.

- Scot

Could you add link to list item to see details about item.

- Mike

@Scot,

Sure you can. You just need to add the required HTML:

<li><a href="/Details/@item.ID">@item</a></li>

- Scot

Thanks,Mike I found solution.

- Andrea

thanks a lot Mike. Very usefull.
Andrea

- Emilio

Hello, sorry for my noob question, but I'm starting to learn C # and asp.net. I'm trying to implement this in a small contact system, like the phonebook, but I'm getting a reference error in "EFRecipeContext ()". What would "EFRecipeContext ()" in this context? for what I would replace in my project?

- Mike

@Emilio,

You should replace it with the name of your context.

Recent Comments

Pam 30/08/2017 11:30
In response to Sending Email in Razor Pages
Mike, RazorPages sound like a nice choice for somebody still working in ASP classic who wants to to...

Robby Robson 15/08/2017 00:43
In response to Routing in Razor Pages
Mike: great stuff. Now that .Core Standard 2.0 is formally out, how soon will you rewrite your book...

Satyabrata Mohapatra 28/07/2017 08:59
In response to Sending Email in Razor Pages
Bit off topic, but congratulation sir for your MVP award. You deserve it !!!...

Satyabrata Mohapatra 23/07/2017 16:43
In response to Razor Pages - The Elevator Pitch
@Dale Severin You can continue to build apps using asp.net web pages....

Satyabrata Mohapatra 23/07/2017 16:40
In response to Sending Email in Razor Pages
Thanks for sharing...learned a lot...

Gfw 22/07/2017 11:53
In response to Sending Email in Razor Pages
Question... Does System.Net.Mail support SSL?...

Dale Severin 20/07/2017 03:38
In response to Razor Pages - The Elevator Pitch
I work with razor web pages extensively. I appreciate the rapid development it permits me to I am as...

Obinna Okafor 14/07/2017 01:19
In response to Routing in Razor Pages
Thank you, Mike. Good post....

Satyabrata Mohapatra 11/07/2017 16:02
In response to Routing in Razor Pages
Very powerful routing system!!...

Cyrus 05/07/2017 03:41
In response to Razor Pages - Getting Started With The Preview
How can I trim packages and services as much as possible to use just razor pages? I don’t want to to...