Making spiffy buttons with CSS and MVC Razor helpers

Posted by Joe Wilson on Sunday, September 16, 2012 2:07 PM

For most web apps, gray buttons with black text is fine.  But if you take some time to jazz up your buttons a little, you are making your app a little nicer to use and giving your users small visual cues to let them know their options.

Going from this:

image

to this:

image

image

with hover colors of green and red, which, in the U.S. at least, usually mean “go” and “stop”, can be just enough of a nudge to help your users and make them just a little happier.  Here’s how I built these.

Credit

The bulk of the concept and CSS work for this came from someone else, but I can’t remember who anymore.  It’s been over a year since I started using this technique.  It might be from an old version of HTML5 Boilerplate, Blueprint CSS, 960 Grid System, or something Rob Conery did on TekPub.  I wish I could give more specific attribution, but it was probably one of these.  My main contributions have been some CSS tweaks, the JavaScript, and the MVC Razor helpers.

HTML and images

This is what we’re going for as the emitted HTML.  We want these “buttons” to really be hyperlinks, not HTML buttons, so we don’t have as much browser style sheet nonsense getting in the way.

This does add some complexity for submitting, which has to be done with JavaScript now, but the same script (see below) can be used throughout the app.

The positive CSS class gives the green hover effect, and the negative one gives the red effect.

<div class="buttons">
    <a href="#" class="button positive">
        <img width="16" height="16" src="/Content/Images/Icons/tick.png" alt="">
        Save
    </a>
    <a href="/Home" class="button negative">
        <img width="16" height="16" src="/Content/Images/Icons/cross.png" alt="">
        Cancel
    </a>
</div>

The icons I used are from the Silk Icons collection.  There are lots of great icon libraries out there, so use your favorite.  Anything 16x16 should work, but you can go bigger or smaller and adjust the CSS and HtmlHelper tags.

JavaScript/jQuery

An anchor tag with a href to “#” won’t get your values submitted back to the server, so we’ll use some JavaScript and a touch of jQuery for that.  You’ll want to include this script on every page where you’re using these buttons.

$(function () {
    global.initButtons();
});

var global = {
    initButtons: function () {
        $('a.button.positive').click(function (e) {
            e.preventDefault();
            var parentForm = $(this).closest('form');
            if (!parentForm.valid()) {
                global.flashMessage('Error validating these values', 'error');
            }
            else {
                $(this).css(global.disableCss).unbind('click');
                parentForm.submit();
            }
        });
    },
    disableCss: { opacity: 0.5, display: 'inline-block' }
};

CSS

The CSS for this is pretty simple.  The div.buttons puts the buttons on one line and give a little vertical space around them.  The .button and .button img styles do most of the work, with block styling, light and medium gray borders, a light gray background, some padding around the text and icon to fatten the button up a little, margins so there is space before the next adjacent button on the right, and a small shadow effect.

The :hover effects are where the color is flipped to darker gray, green, or red, with corresponding border colors.

/*--------------------------*/
/* Buttons
/*--------------------------*/
div.buttons
{
    display: inline-block;
    margin: 10px 0;
}
.button
{
    display: block;
    float: left;
    border-top: 1px solid #CCC;
    border-left: 1px solid #CCC;
    border-bottom: 1px solid #AAA;
    border-right: 1px solid #AAA;
    background-color: #E3E3E3;
    font-family: Verdana, Helvetica, sans-serif;
    font-size: 100%;
    line-height: 110%;
    text-decoration: none !important;
    color: #333 !important;
    margin: 0 15px 0 0;
    padding: 5px 10px 5px 7px;
    cursor: pointer;
    box-shadow: 1px 1px 2px #969696;
    -moz-box-shadow: 1px 1px 2px #969696; /* Firefox */
    -webkit-box-shadow: 1px 1px 2px #969696; /* Safari and Chrome */
}
.button img
{
    margin: 0 3px -3px 0 !important;
    padding: 0;
    border: none;
    width: 16px;
    height: 16px;
    float: none;
}

/*--------------------------*/
/* Button colors - Standard
/*--------------------------*/
.button:hover
{
    border: 1px solid #999;
    background-color: #D6D6D6;
    color: #333 !important;
}

/*--------------------------*/
/* Button colors - Positive
/*--------------------------*/
.positive:hover
{
    background-color: #E6EFC2;
    border: 1px solid #C6D880;
    color: #264409 !important;
}

/*--------------------------*/
/* Button colors - Negative
/*--------------------------*/
.negative:hover
{
    background-color: #FBE3E4;
    border: 1px solid #FBC2C4;
    color: #8A1F11 !important;
}

MVC Razor HtmlHelpers

So now we know the HTML and CSS we want.  You could stop there and code your MVC views with that HTML.  But if you want your Razor views to be able to user simple helpers like this, keep reading.

<div class="buttons">
    @Html.LinkButtonForSave()
    @Html.LinkButtonForCancel()
</div>

The helpers here have two public entry points, LinkButtonForSave and LinkButtonForCancel. The rest of the code is refactoring out some of the route parsing and tag building work.  It’s probably overkill for just these two methods, but it makes sense when you keep adding helpers for other buttons, like add, edit, delete, confirm, print, etc.

using System.Web;
using System.Web.Mvc;

namespace Extensions
{
    public static class ButtonHelpers
    {
        public static MvcHtmlString LinkButtonForSave(this HtmlHelper helper)
        {
            var imageTag = ImageTag("tick.png");
            var anchorHtml = FormSubmitAnchorTag(imageTag, "Save");
            return MvcHtmlString.Create(anchorHtml);
        }
        
        public static MvcHtmlString LinkButtonForCancel(this HtmlHelper helper)
        {
            var areaName = AreaName(helper);
            var controllerName = ControllerName(helper);
            var imageTag = ImageTag("cross.png");
            var anchorHtml = AnchorTagWithImageAndText(areaName, controllerName, "Index", null, "button negative", imageTag, "Cancel");
            return MvcHtmlString.Create(anchorHtml);
        }

        private static TagBuilder ImageTag(string iconName)
        {
            var imageTag = new TagBuilder("img");
            imageTag.MergeAttribute("src", VirtualPathUtility.ToAbsolute(string.Format("~/Content/Images/Icons/{0}", iconName)));
            imageTag.MergeAttribute("width", "16");
            imageTag.MergeAttribute("height", "16");
            imageTag.MergeAttribute("alt", "");
            return imageTag;
        }

        private static string AreaName(HtmlHelper helper)
        {
            var routeData = helper.ViewContext.RouteData;
            return routeData.DataTokens["area"] == null ? string.Empty : routeData.DataTokens["area"].ToString();
        }

        private static string ControllerName(HtmlHelper helper)
        {
            var routeData = helper.ViewContext.RouteData;
            return routeData.GetRequiredString("controller");
        }

        private static string FormSubmitAnchorTag(TagBuilder imageTag, string anchorText)
        {
            var anchorTag = new TagBuilder("a");
            anchorTag.MergeAttribute("href", "#");
            anchorTag.AddCssClass("button positive");
            anchorTag.InnerHtml = imageTag.ToString(TagRenderMode.SelfClosing) + anchorText;
            return anchorTag.ToString();
        }

        private static string AnchorTagWithImageAndText(string areaName, string controllerName, string actionName, int? id, string cssClass, TagBuilder imageTag, string anchorText)
        {
            var anchorTag = new TagBuilder("a");
            if (areaName != string.Empty)
            {
                anchorTag.MergeAttribute("href", id == null
                                                     ? string.Format("/{0}/{1}/{2}", areaName, controllerName, actionName)
                                                     : string.Format("/{0}/{1}/{2}/{3}", areaName, controllerName, actionName, id));               
            }
            else
            {
                anchorTag.MergeAttribute("href", id == null
                                                     ? string.Format("/{0}/{1}", controllerName, actionName)
                                                     : string.Format("/{0}/{1}/{2}", controllerName, actionName, id));
            }
            anchorTag.AddCssClass(cssClass);
            anchorTag.InnerHtml = imageTag.ToString(TagRenderMode.SelfClosing) + anchorText;
            return anchorTag.ToString();
        }
    }
}

Tags: CSS, MVC, Html Helpers
Categories: Technical

MVC 2: Electric Boogaloo - What's new in MVC 2

Posted by Joe Wilson on Sunday, January 17, 2010 3:28 PM

The MVCbreakin2 team loves their rec center, but an evil real estate developer wants to bulldoze it to build a hipster bar named Ruby's.  The team has been coding their butts off with MVC 2, but will the new features be enough to save the rec center?  Will the cynical real estate developer cackle and complain that it's still not enough? 

Will the MVC team get areas right?  Will the new templated HtmlHelpers strike the right balance of rapid development and fine control?  Will validation concerns get muddled up with UI concerns?  Will more web forms developers make the switch to MVC? 

New Areas

Areas are an organizing tool for large web sites.  If you've got lots of view and lots of controllers plus some shared content, areas can work as a top-level routing mechanism. 

Example areas might be Admin for an administrative site, Store for a ecommerce site, etc.  They can be either folders or separate web projects.  Use the AreaRegistration class to set up your routes in your Global.asax (taken from MSDN article):

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    AreaRegistration.RegisterAllAreas();

    routes.MapRoute(
        "Default",                                              // Route name
        "{controller}/{action}/{id}",                           // URL with parameters
        new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
    );
}

Then override AreaRegistration in the area's folder or in the root of the area web project (also taken from MSDN article):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace Store
{
    public class Routes : AreaRegistration
    {
        public override string AreaName
        {
            get { return "Store"; }
        }

        public override void RegisterArea(AreaRegistrationContext context)
        {
            context.MapRoute(
                "Store_Default",
                "Store/{controller}/{action}/{id}",
                new { controller = "Products", action = "List", id = "" }
            );
        }
    }
}

Sharp Architecture has had areas since its first release.  I remember hearing rumors about areas coming in MVC for version 1.0, but I guess it wasn't ready.  The biggest difference between the Microsoft version and the Sharp Architecture version is that the Microsoft version lets you register different web projects. 

Theoretically, multiple teams can work on multiple web projects, then these web projects can be rolled into the same solution and AreaRegistration can make them appear as one big web site to a browser.  I don't know how much of a need that is, but I guess if you have distributed teams that can't get to the same source code repository, it might be useful.  I prefer multiple folders in one web project over multiple web projects to keep build times down and Visual Studio responsiveness up.  I wouldn't break out each logical web site section to its own web project until the folder approach stopped working.

New HtmlHelpers

I'm not sure if Code Camp Server pushed the MVC team into this or just showed how it was possible to take HtmlHelpers to this extreme, but I'm glad to see the same concepts getting rolled into the base class libraries. 

I think the new HtmlHelpers do give a nice range of control over your markup.  You can use:

<%= Html.LabelFor(x => x.FirstName) %>
<%= Html.EditorFor(x => x.FirstName) %>
<%= Html.LabelFor(x => x.LastName) %>
<%= Html.EditorFor(x => x.LastName) %>

if you want to be fairly explicit for each form element.  Or you can save a lot of keystrokes with:

<%= Html.EditorFor(x => x) %>

And if that's not enough, you can decorate your models with some UI hint attributes and roll your own rendering.  I think this is something large web projects will take advantage of for consistent markup and control rendering.  Seems like this has the advantages of homemade server controls without some of the hassles (INamingContainer, I'm looking at you).

New Validation

I did worry for a little bit about the UI stuff bleeding into the model with these new attributes.  Seems like we've got a leaky abstraction there.  But these days I think of the MVC model as the view model, not the domain model.  The view model is married to the view it works with.  It has no other purpose than to represent the data in that view.  Well, now it also validates the data in that view, at least initially.  Oh, and it now controls some of the view rendering.  Damn.  That's three responsibilities for my little view model.

But I don't have an alternative proposal to attribute-heavy view models either.  I don't want partial classes.  I haven't seen much benefit to the buddy class approach.  And I do want to reuse some of my UI hints across view models (like a date picker for DateTime types).  Plus, I want to take advantage of the new client and server side validation of my view model before it gets into the action method.  MVC 2 has this built in.

I guess I'll grin and bear it and work on a new blog post about the Triple Responsibility Principal for View Models (as opposed to SRP). :D

Future

I've already talked about where MVC views are now and where they are going, and the view engine is definitely getting better. I still think views will continue to be the most in-flux part of the MVC framework.  I know people that are very happy with Spark as a view engine, and Louis DeJardin, the guy who wrote Spark, is now working for Microsoft on the ASP.NET team.  I don't have any inside scoop, but I wouldn't be surprised if the web forms view engine got a little."sparkier".

Views may also be the area with the most room for improvement, depending on who you talk to.  When I visit with people about MVC and explain that runat="server" is gone and you can go ahead and close your Visual Studio toolbox, most get a glazed look.  They don't want to hand code the HTML.  They don't want to see a property sheet that doesn't know anything about their current mouse selection.  They don't want to give up their current productivity.

I get that, but I think you can be faster without the designers and toolboxes and property sheets.  It takes a while to get used to using the keyboard more than the mouse, but I think it's worth pushing yourself over that learning curve.

Time to switch from Web Forms to MVC?

I push MVC on projects where I have influence on that decision.  In your world, it's your call.  You may have environment constraints, in-flight projects, etc. 

But it also doesn't have to be all or nothing.  You don't have to wait for a greenfield project to fall in your lap.  You can run MVC inside your current Web Forms project.  It's just another set of HttpHandlers living under ASP.NET.  Here is some guidance for getting your current project set up to do this.

I encourage you to give it a try and see what you think.  It will take a while to get used to the paradigm.  It's not hard - just different.  But I think once you're used to it, you'll like the separation of concerns, the easier testing, and the more natural flow for web development. 

By natural flow, I mean embracing the world of GET and POST.  I mean when you need to expose JSON, an RSS feed, or something else (XML, PDF, file, etc.), Controller Actions are ready to go.  Stop writing custom HttpHandlers or fighting WCF configuration for this stuff.  It should be simpler, and it is with Controller Actions and the right ActionResult.

Make it a goal to get some real-world MVC experience in 2010.  Think of the kids on the MVC team and their beloved rec center.  Don't let their dream die.  They've come too far.  And remember the tag line from Breakin' 2 - "If you can't beat the system.break it!".

Tags: MVC, Web Forms, Html Helpers, Data Annotations, Validation, Areas
Categories: Technical

Evolution of a View in ASP.NET MVC

Posted by Joe Wilson on Wednesday, August 12, 2009 1:42 PM

Many developers prefer working with ASP.NET MVC over Web Forms because they are more connected with the HTML, have better control over the rendered output, and can easily build their own HTML helpers to get consistent output.

But making the views can be a hassle when your building a CRUD app or something that has a lot of very similar views.  Let's look at some ways around that you can use today and in the future.

Past

When MVC first came out, most people were coding their inputs in views like this.  Just straight HTML with some helpers:

<% using (Html.BeginForm()) { %>
	<fieldset>
		<label for="Name">Name</label>
		<%= Html.TextBox("Name") %>
		<label for="Email">Email address</label>
		<%= Html.TextBox("Email") %>
		<label for="Phone">Phone</label>
		<%= Html.TextBox("Phone") %>
		<%= Html.SubmitButton() %>
	</fieldset>
<% } %>

Simple, but a little too much reliance on strings.  This made the code error prone, so developers made their own HTML Helpers to crank out consistent views with fewer strings by using lambda expressions:

<% using (Html.BeginForm()) { %>
	<fieldset>
		<label for="Name">Name</label>
		<%= Html.TextBoxFor(c => c.Name) %>
		<label for="Email">Email address</label>
		<%= Html.TextBoxFor(c => c.Email) %> 
		<label for="Phone">Phone</label>
		<%= Html.TextBoxFor(c => c.Phone) %> 
		<%= Html.SubmitButton() %>
	</fieldset>
<% } %>

That's better.  We get IntelliSense for the model's fields and we get the leave some of the strings behind.

Present

The next step was to get rid of some of those label statements and get those rolled into the output automatically.  They did this in Code Camp Server and you can see the step-by-step progression in Eric Hexter's blog:

<% using (Html.BeginForm()) { %>
	<fieldset>
		<%= Html.Input(c => c.Name) %>
		<%= Html.Input(c => c.Email) %> 
		<%= Html.Input(c => c.Phone) %> 
		<%= Html.SubmitButton() %>
	</fieldset>
<% } %>

Nice!  Now we're on the road to consistent output in all forms in our MVC app and we won't spend as much time typing up view code.  Eric even shows us he can name that view in one note:

<%= Html.InputForm() %>

Future

If you've been following the latest on ASP.NET MVC, you've seen the announcement for MVC 2 Preview 1 here, here, and here.  Microsoft is going the same direction as these open-source extensions and making it easier to get a consistent view rendered:

<% using (Html.BeginForm()) { %>
	<fieldset>
		<%= Html.LabelFor(c => c.Name) %>
		<%= Html.EditorFor(c => c.Name) %>
		<%= Html.LabelFor(c => c.Email) %>
		<%= Html.EditorFor(c => c.Email) %> 
		<%= Html.LabelFor(c => c.Phone) %>
		<%= Html.EditorFor(c => c.Phone) %> 
		<%= Html.SubmitButton() %>
	</fieldset>
<% } %>

MVC 2 also has an almost one-liner form that you define with templates, one of the new features:

<% using (Html.BeginForm()) { %>
	<fieldset>
		<%= Html.EditorFor(c => c) %>
		<%= Html.SubmitButton() %>
	</fieldset>
<% } %>

So we've seen views getting simpler and smaller and even one-line forms.  Can it get any smaller than a one-line form?  What if there was no view?  Can you have a view with zero lines of code?

Kind of.  Phil Haack took the MVC 2 Preview 1 code out for a spin to try what he calls "default templated views".  The idea is that your controller gets a model and displays a virtual view by using templates from your web project's "~\Views\Shared" folder.  You can make as many templates as you need for different purposes.

This is ideal for CRUD apps or places in your app where the view is the same except for the model data it renders.  You don't need a physical file for each view other than the template itself.  The controller just renders a view that is one of the shared views/templates.

I like this approach because you can crank out code more quickly for the easy stuff, but if you have a view that needs something more complex, you still have the normal HTML inputs and helpers to fall back on.  I hope Microsoft gets this idea fully flushed out and it ends up in the final MVC 2 release.  It would make developers more productive on repetitive views and would have wider acceptance if it was in the MVC base-class libraries.

Tags: MVC, Html Helpers
Categories: Technical

Blog links

  • Subscribe to this blogRSS feed
  • Archive of old posts

Popular posts

  • Autocomplete dropdown with jQuery UI and MVC
  • Handling Exceptions in ASP.NET MVC
  • Don't mock HttpContext
  • Review of Sharp Architecture
  • Evolution of a View in ASP.NET MVC
  • Comparison of Typemock Isolator and Rhino Mocks
  • Building a Windows 8 Live Tile with JavaScript
  • Setting Default Values for Multiple Value Parameters in Reporting Services
  • Buy, Build, or Both?
  • Why bother writing unit tests?

Tag cloud

  • AppHarbor
  • Areas
  • ASP.NET
  • ATDD
  • BDD
  • Castle Windsor
  • Coding Standards
  • Common Service Locator
  • continuous integration
  • Cookies
  • CRM
  • CSS
  • Custom Software
  • Data Annotations
  • DataTables
  • DDD
  • Dell
  • Dependency Injection
  • DTOs
  • ELMAH
  • git
  • GitHub
  • Html Helpers
  • HttpContext
  • IOC
  • iPad
  • iPhone
  • JavaScript
  • jQuery
  • jQuery Mobile
  • JSON
  • Kendo UI
  • Knockout
  • Microsoft Accounting
  • Moq
  • MVC
  • NHibernate
  • NuGet
  • NUnit
  • OData
  • optimizations
  • Patterns
  • POCOs
  • QuickBooks
  • Rails
  • Refactoring
  • Reporting Services
  • REST
  • Rhino Mocks
  • Session
  • Sharp Architecture
  • SOLID
  • SpecFlow
  • SQL Server
  • SSRS
  • TDD
  • TeamCity
  • TempData
  • Typemock
  • unit testing
  • Validation
  • Visual Studio
  • VMWare
  • WatiN
  • WCF
  • Web API
  • Web Essentials
  • Web Forms
  • Windows 7
  • Windows 8
  • WinJS

Archive

  • 2013
    • May (1)
    • April (1)
    • March (1)
    • February (3)
    • January (1)
  • 2012
    • December (1)
    • October (6)
    • September (3)
    • March (1)
  • 2011
    • October (1)
    • August (1)
    • June (3)
    • March (2)
    • February (2)
    • January (4)
  • 2010
    • December (2)
    • October (3)
    • September (1)
    • August (2)
    • July (1)
    • May (1)
    • April (2)
    • March (2)
    • February (3)
    • January (2)
  • 2009
    • November (3)
    • October (2)
    • September (5)
    • August (2)
    • July (3)

Blogroll

  • RSS feed for Dan WahlinDan Wahlin
  • RSS feed for Jimmy BogardJimmy Bogard
  • RSS feed for John PapaJohn Papa
  • RSS feed for Josh TwistJosh Twist
  • RSS feed for Los TechiesLos Techies
  • RSS feed for Phil HaackPhil Haack
  • RSS feed for Scott GuthrieScott Guthrie
  • RSS feed for Scott HanselmanScott Hanselman
  • RSS feed for Steve SandersonSteve Sanderson

Twitter

  • Twitter May 15, 7:20 PM

    At Denver .NET meetup to hear @rlacovara talk about SpecFlow

  • Twitter May 15, 9:26 AM

    @eriklane @extofer @greeleygeek That sounds weird to me, too. Values on the query string, sure, but not JSON.

  • Twitter May 15, 9:24 AM

    @kevinkrueger otherwise, people will not be as forthcoming about areas they hope the team can improve

  • Twitter May 15, 9:24 AM

    @kevinkrueger I think retros are best if they are for the team only, so they can have a frank discussion of how to get better.

  • Twitter May 14, 10:49 AM

    Terrific talk from @zekeli @html5denver last night "Cross domain Pong with window.postMessage" http://t.co/iO0AAlbJ7l http://t.co/5mnf2fZL0p

  • Follow me on TwitterFollow me on Twitter

Recognition

  • INETA Community Champions

Blog license

  • Creative Commons License
    Blog by Volare Systems is licensed under a Creative Commons Attribution 3.0 Unported License.
    Based on a work at http://volaresystems.com/blog/.