ASP.NET MVC Optimization Series: Part 4 - Simulate a Native ListView

July 20th, 2015

This series discusses how we build a web application into a super-charged web app that acts like a native application. Today, we continue by showing how to build a list of speakers who presented at Codemash.

In our last post (and it was a long one), we demonstrated a way to make your ASP.NET MVC web application function almost like a native application in the ways of speed and user interface.

Now we focus on the list of speakers and provide a nice list view interface that shows us what sessions they are presenting at Codemash.

While this post may be short, I'm trying to pace these articles so they don't overwhelm like the last one. If you like the long posts, make your opinion known in the comments below (and thanks!).

Ok, let's do this!

Speakers...and Action!

The Speaker Action controller will show the entire list of speakers with their avatar. The avatar is provided through Gravatar.com. You provide a simple email address and a picture and from that point, the Internet will associate the name with the face. :-)

As much as I want to cache any picture on the site, at this time I couldn't determine a way to cache all of the images from gravatar. I'm going to assume they use a cache or CDN of some kind.

First, we need our controller for speakers. We already have our Url Helper from the last post. Now we need our action method for our Speakers and SpeakerDetail.

public ActionResult Speakers()
{
    return View(_factory.GetViewModel<CodemashController, CodemashSpeakerViewModel>(this));
}
 
public PartialViewResult SpeakerDetail(string id)
{
    var speakerId = id;
    return PartialView("_partialSpeaker",
        _factory.GetViewModel<CodemashController, SpeakerDetailViewModel, string>(this, speakerId));
}

Of course, we are using our single-line ViewModelBuilder. Our CodemashSpeakerViewModel is returning our list of Speakers along with a list of sessions.

public class CodemashSpeakerViewModelBuilder : BaseViewModelBuilder, 
    IViewModelBuilder<CodemashController, CodemashSpeakerViewModel>
{
    public CodemashSpeakerViewModel Build(CodemashController controller, 
        CodemashSpeakerViewModel viewModel)
    {
        var title = "Codemash v2.0.1.5 - Speakers";
        viewModel.Title = viewModel.MetaKeywords = title;
 
        var codemashUnitOfWork = new CodemashUnitOfWork();
        var sessions = codemashUnitOfWork.SessionRepository.GetAll();
            
        viewModel.Speakers = codemashUnitOfWork.SpeakerRepository.GetAll();
        foreach (var speaker in viewModel.Speakers)
        {
            speaker.Sessions = 
                new List<Session>(sessions.Where(e => 
                    e.Speakers.Any(f => f.Id == speaker.Id)));
        }            
 
        return viewModel;
    }
}

Nothing new with the ViewModelBuilder except where we are assigning the sessions to each speaker. Since the Sessions are cached, we won't need a database...errr...web service call to pull the sessions.

Our SpeakerDetailViewModelBuilder isn't any different except for the specific speaker and, again, since we are pulling the cached version of the Speakers, we don't need to worry about any network latency.

public class SpeakerDetailViewModelBuilder : BaseViewModelBuilder, 
    IViewModelBuilder<CodemashController, SpeakerDetailViewModel, string>
{
    public SpeakerDetailViewModel Build(CodemashController controller, 
        SpeakerDetailViewModel viewModel, string id)
    {
        var codemashUnitOfWork = new CodemashUnitOfWork();
        var sessions = codemashUnitOfWork.SessionRepository.GetAll();
        viewModel.Speaker = codemashUnitOfWork.SpeakerRepository
            .GetAll()
            .FirstOrDefault(e=> e.Id == id);
 
        if (viewModel.Speaker != null)
        {
            viewModel.Speaker.Sessions = new List<Session>();
            var speakerSessions = sessions.Where(e => 
                e.Speakers.Any(f => f.Id == id));
            if (speakerSessions.Any())
            {
                viewModel.Speaker.Sessions.AddRange(speakerSessions);
            };
        }
 
        viewModel.Title = String.Format("CodeMash Speaker: {0} {1}", 
            viewModel.Speaker.FirstName, viewModel.Speaker.LastName);
 
        return viewModel;
    }
}

There! We have our actions set up in our controllers. Now, we can move onto our HTML Views.

Show me your Views!

As before in our Sessions View, we need two views: One for the list of Speakers and one for the Speaker details.

Our Speakers View uses a mix of Bootstrap's media object and a List Group.

Views/Codemash/Speakers.cshtml

@model CodemashApp.ViewModel.CodemashSpeakerViewModel
 
<div class="speaker-list">
    @foreach (var speaker in Model.Speakers.OrderBy(e => e.LastName))
    {
        @Html.Partial("_SpeakerListItem", speaker)
    }
</div>
<div class="speaker-detail"></div>
<div class="text-center loading-status">
    <i class="fa fa-3x fa-spin fa-spinner"></i>
</div>

Views/Shared/_speakerListItem.cshtml

@using CodemashApp.Helpers.Url
@model CodemashApp.Models.Speaker
 
 
<a name="@Model.Id" href="javascript:void(0);" data-target="@Url.SpeakerDetailUrl(Model.Id)" class="list-group-item speaker">
    <div class="media">
        <div class="media-object pull-left">
            <img src="@Model.GravatarUrl" alt="@Model.FirstName @Model.LastName"
                 class="speaker-img img-rounded" />
        </div>
        <div class="media-body">
            <h4 class="media-heading">@Model.FirstName @Model.LastName</h4>
            <p><span class="label label-default">@Model.Sessions.ToList().Count() Sessions</span> </p>
        </div>
    </div>
</a>

Finally, we have our speaker's detail page. This detail page is when they click on the speaker's name and it dynamically replaces the list with the speaker's details.

Views/Shared/_partialSpeaker.cshtml

@using CodemashApp.Helpers.Html
@model CodemashApp.ViewModel.SpeakerDetailViewModel
 
<a href="javascript:void(0);" class="back-button"><i class="fa fa-angle-double-left"></i> Back</a>
 
<div class="well">
    <div class="text-center">
        <img src="@Model.Speaker.GravatarUrl" title="@Model.Speaker.FirstName @Model.Speaker.LastName" alt="@Model.Speaker.FirstName @Model.Speaker.LastName"
             class="img-rounded" />
        <h3>@Model.Speaker.FirstName @Model.Speaker.LastName</h3>
        @Html.DisplaySocialIcons(Model.Speaker)
    </div>
    <p>
        @Model.Speaker.Biography
    </p>
 
    <h4 class="list-group-item-heading">Sessions</h4>
    <div class="list-group">
        @foreach (var session in Model.Speaker.Sessions)
        {
            <div class="list-group-item">
                <h4 class="list-group-item-heading">@session.Title</h4>
            </div>
        }
    </div>
 
</div>

Help me Helper!

We also have an HtmlHelper hidden in our code from above called DisplaySocialIcons. In the speaker's profile, we have the speaker's GitHub, Blog, LinkedIn, and Twitter account Urls.

We definitely want our audience to get in touch with our speakers. So we place a list of icons underneath the speaker's avatar with their biography.

Helpers/Html/SpeakerItemHelper.cs

public static class SpeakerItemHelper
{
    public static MvcHtmlString DisplaySocialIcons(this HtmlHelper helper, Speaker speaker)
    {
        if (speaker == nullreturn MvcHtmlString.Empty;
 
        var ul = new TagBuilder("ul");
        ul.AddCssClass("list-unstyled list-inline");
 
        var icons = AddSocialIcon(speaker.BlogUrl, "link").ToHtmlString() +
                    AddSocialIcon(speaker.GitHubLink, "github-square") +
                    AddSocialIcon(speaker.LinkedInProfile, "linkedin-square") +
                    AddSocialIcon(speaker.TwitterLink, "twitter-square");
 
        ul.InnerHtml = icons;
 
        return MvcHtmlString.Create(ul.ToString(TagRenderMode.Normal));
    }
 
    private static MvcHtmlString AddSocialIcon(string typeUrl, string faIcon)
    {
        if (String.IsNullOrEmpty(typeUrl)) return MvcHtmlString.Empty;
 
        var li = new TagBuilder("li");
 
        var anchor = new TagBuilder("a");
        anchor.Attributes.Add("href",typeUrl);
        anchor.Attributes.Add("title",typeUrl);
        
        var icon = new TagBuilder("i");
        icon.AddCssClass(String.Format("fa fa-{0} fa-3x", faIcon));
 
        anchor.InnerHtml = icon.ToString(TagRenderMode.Normal);
 
        li.InnerHtml = anchor.ToString(TagRenderMode.Normal);
        return MvcHtmlString.Create(li.ToString(TagRenderMode.Normal));
    }
}

As mentioned before, we are using FontAwesome instead of the Bootstrap's Glyph icons. I like the ability to size FontAwesome's icons to whatever fits the context.

Once we have all of this in place, we can run our web app to see our progress so far.

Quick! To The JavaScript!

If you remember, we used JavaScript to make our loading of HTML a little more bearable since we may experience some network lag when pulling our partial View.

Our JavaScript will look very similar to our other Session click event, but since our HTML is a little different, we need to write a similar speaker click event using jQuery.

Scripts/codemash.js

$("a.speaker").on("click"function (e) {
    e.preventDefault();
    var url = $(this).attr("data-target");
    jumper = $(this).attr("name");
 
    function speakerDetailLoadCompleted() {
        $(".loading-status").fadeOut("fast"function () {
            $(".speaker-detail").fadeIn("fast");
        });
        $(".back-button").on("click"function (f) {
            f.preventDefault();
            $(".session-detail").fadeOut("fast"function () {
                $(".panel-group").fadeIn("fast");
                var item = $("[name='" + jumper + "']");
                $(document).scrollTop(item.offset().top - 50);
            });
            $(".speaker-detail").fadeOut("fast"function () {
                $(".speaker-list").fadeIn("fast");
                var item = $("[name='" + jumper + "']");
                $(document).scrollTop(item.offset().top - 50);
            });
        });
    }
 
    function speakerListFadedComplete() {
        $(".loading-status").fadeIn("fast"function () {
            $(".speaker-detail").load(url, speakerDetailLoadCompleted);
        });
    }
 
    $(".speaker-list").fadeOut("fast", speakerListFadedComplete);
});

Once we have this included in our Codemash.js file, compile, and run our web application, every time we click on a speaker we load this partial HTML view.

Conclusion

Adding new features to this native app is getting easier and easier now that we have a working version. We just need to think through each feature that we add.

Is there a way to cache the pictures? Can we make something easier to access through AJAX? Can we make the site sexier?

We completed our web application, but something seems to be missing.

In our next part, we'll go through some of the sections of the application and see how we can improve the interface/speed by looking at some simple user interface enhancements.

Did you like the post? Post your comments below.

ASP.NET MVC Optimization Series: