Archive for the ‘ASP.NET’ Category

ASP.NET MVC LinkButton with HtmlHelper extensions

Tuesday, February 22nd, 2011

Download code

We’re really loving using ASP.NET MVC3 for so many reasons, not least the way in which Microsoft seem to have embraced externally developed tools such as jQuery. In Visual Studio the whole experience feels really cohesive and extending it is a breeze as this post will show.

Something we have found missing that seems an unusual oversight is an HtmlHelper extension method for submit buttons. Now I know it’s not much to write:

<input type="submit" value="sign in" />

but it would seem much neater to do this:

@Html.Submit("sign in")

(btw we’re using the razor view engine).

Html.Submit

It’s easy enough to write your own HtmlHelper extension methods for the submit button:

        /// <summary>
        /// Returns an HTML submit button
        /// </summary>
        /// <param name="htmlHelper"></param>
        /// <returns></returns>
        public static MvcHtmlString Submit(this HtmlHelper htmlHelper)
        {
            return htmlHelper.Submit("submit", null);
        }

        /// <summary>
        /// Returns an HTML submit button
        /// </summary>
        /// <param name="htmlHelper"></param>
        /// <param name="value">The text for the button</param>
        /// <returns></returns>
        public static MvcHtmlString Submit(this HtmlHelper htmlHelper, string value)
        {
            return htmlHelper.Submit(value, null);
        }

        /// <summary>
        /// Returns an HTML submit button
        /// </summary>
        /// <param name="htmlHelper"></param>
        /// <param name="value">The text for the button</param>
        /// <param name="htmlAttributes">The html attributes to apply to the button</param>
        /// <returns></returns>
        public static MvcHtmlString Submit(this HtmlHelper htmlHelper, string value, object htmlAttributes)
        {
            // init a tag builder
            TagBuilder tb = new TagBuilder("input");

            // get the attributes
            var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes) as IDictionary<string, object>;

            // set the attributes
            tb.MergeAttributes<string, object>(attributes);

            // set the type
            tb.MergeAttribute("type", "submit", true);

            // set the value
            tb.MergeAttribute("value", value);

            // return self closing
            return new MvcHtmlString(tb.ToString(TagRenderMode.SelfClosing));
        }

Html.SubmitImage

What about an input button (aka the old web forms ImageButton control), well that’s pretty straightforward too:

        /// <summary>
        /// Returns an input type=image
        /// </summary>
        /// <param name="htmlHelper"></param>
        /// <param name="imageUrl">The url of the image</param>
        /// <returns></returns>
        public static MvcHtmlString SubmitImage(this HtmlHelper htmlHelper, string imageUrl)
        {
            return htmlHelper.SubmitImage(imageUrl, null);
        }

        /// <summary>
        /// Returns an input type=image
        /// </summary>
        /// <param name="htmlHelper"></param>
        /// <param name="imageUrl">The url of the image</param>
        /// <param name="htmlAttributes">The html attributes to apply to the button</param>
        /// <returns></returns>
        public static MvcHtmlString SubmitImage(this HtmlHelper htmlHelper, string imageUrl, object htmlAttributes)
        {
            // init a tag builder
            TagBuilder tb = new TagBuilder("input");

            // get the attributes
            var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes) as IDictionary<string, object>;

            // set the attributes
            tb.MergeAttributes<string, object>(attributes);

            // set the type
            tb.MergeAttribute("type", "image", true);

            // set the image url
            tb.MergeAttribute("src", imageUrl);

            // set the alt attribute
            tb.MergeAttribute("alt", "submit");

            // return self closing
            return new MvcHtmlString(tb.ToString(TagRenderMode.SelfClosing));
        }

And you can now use this like so:

@Html.SubmitImage(Url.Content("~/url/to/image.png"))

Html.SubmitLink

Ok, so far so good, what about the good old LinkButton? I’ve seen several posts around the internet that we should be using ActionLink, but that doesn’t submit the form. So, using the principles of unobtrusive javascript we create the following extension:

        /// <summary>
        /// Returns an A tag with a nested SPAN used to submit a form, requires jquery.link-button.js
        /// </summary>
        /// <param name="htmlHelper"></param>
        /// <returns></returns>
        public static MvcHtmlString SubmitLink(this HtmlHelper htmlHelper)
        {
            return htmlHelper.SubmitLink("submit");
        }

        /// <summary>
        /// Returns an A tag with a nested SPAN used to submit a form, requires jquery.link-button.js
        /// </summary>
        /// <param name="htmlHelper"></param>
        /// <param name="linkText">The text for the link button</param>
        /// <returns></returns>
        public static MvcHtmlString SubmitLink(this HtmlHelper htmlHelper, string linkText)
        {
            return htmlHelper.SubmitLink(linkText, null);
        }

        /// <summary>
        /// Returns an A tag with a nested SPAN used to submit a form, requires jquery.link-button.js
        /// </summary>
        /// <param name="htmlHelper"></param>
        /// <param name="linkText">The text for the link button</param>
        /// <param name="htmlAttributes">The html attributes to apply to the button</param>
        /// <returns></returns>
        public static MvcHtmlString SubmitLink(this HtmlHelper htmlHelper, string linkText, object htmlAttributes)
        {
            // init a tag builder
            TagBuilder tb = new TagBuilder("a");

            // get the attributes
            var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes) as IDictionary<string, object>;

            // set the attributes
            tb.MergeAttributes<string, object>(attributes);

            // add the data attribute
            tb.MergeAttribute("data-link-button", "true");

            // add a nothing href
            tb.MergeAttribute("href", "#");

            // set the inner html
            tb.InnerHtml = String.Format("<span>{0}</span>", linkText);

            // return self closing
            return new MvcHtmlString(tb.ToString());
        }

You will notice that the markup rendered by this contains a span nested within the a tag. We implemented the extension method like this to make it easy for us to do style the buttons nicely using the sliding doors technique.

Now, this won’t work without the associated javascript file (jquery.link-button.js):

/// <reference path="jquery-1.5.js" />
(function ($) {
    $("a[data-link-button=true]").live("click", function (e) {
        e.preventDefault();
        var form = $(this).parents('form').first().submit();
        return false;
    });
})(jQuery);

As you can see this relies on jQuery so you’ll need a reference to that too.

Download code

Custom ValidationAttribute for comparing properties.

Friday, January 14th, 2011

We’re using ASP.NET MVC3 on our latest project and have been really impressed so far.  One of the coolest things I have come across is using DataAnnotations to decorate the model classes and having the framework wire up all the validation and error messages.  It is truly great – you can find out more at Scott Guthrie’s blog post on the subject (for MVC2 but the principles are the same).

There’s no built in support for comparing two properties though this has been addressed to a certain extent by the CompareAttribute in the System.Web.Mvc namespace in MVC3, David Hayden has blogged about it here.  This attribute allows you to specify that the value of two properties should be equal, but doesn’t help with greater than or less than operations.  This kind of thing would be really useful to specify that the StartDate of an Appointment should be before the EndDate.

So I have written my own custom comparison validation attribute for use with asp.net mvc in the style of the built in data annotations.

    /// <summary>
    /// Specifies that the field must compare favourably with the named field, if objects to check are not of the same type
    /// false will be return
    /// </summary>
    [AttributeUsageAttribute(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
    public class CompareValuesAttribute : ValidationAttribute
    {
        /// <summary>
        /// The other property to compare to
        /// </summary>
        public string OtherProperty { get; set; }

        public CompareValues Criteria { get; set; }

        /// <summary>
        /// Creates the attribute
        /// </summary>
        /// <param name="otherProperty">The other property to compare to</param>
        public CompareValuesAttribute(string otherProperty, CompareValues criteria)
        {
            if (otherProperty == null)
                throw new ArgumentNullException("otherProperty");

            OtherProperty = otherProperty;
            Criteria = criteria;
        }

        /// <summary>
        /// Determines whether the specified value of the object is valid.  For this to be the case, the objects must be of the same type
        /// and satisfy the comparison criteria. Null values will return false in all cases except when both
        /// objects are null.  The objects will need to implement IComparable for the GreaterThan,LessThan,GreatThanOrEqualTo and LessThanOrEqualTo instances
        /// </summary>
        /// <param name="value">The value of the object to validate</param>
        /// <param name="validationContext">The validation context</param>
        /// <returns>A validation result if the object is invalid, null if the object is valid</returns>
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            // the the other property
            var property = validationContext.ObjectType.GetProperty(OtherProperty);

            // check it is not null
            if (property == null)
                return new ValidationResult(String.Format("Unknown property: {0}.", OtherProperty));

            // check types
            if (validationContext.ObjectType.GetProperty(validationContext.MemberName).PropertyType != property.PropertyType)
                return new ValidationResult(String.Format("The types of {0} and {1} must be the same.", validationContext.DisplayName, OtherProperty));

            // get the other value
            var other = property.GetValue(validationContext.ObjectInstance, null);

            // equals to comparison,
            if (Criteria == CompareValues.EqualTo)
            {
                if (Object.Equals(value, other))
                    return null;
            }
            else if (Criteria == CompareValues.NotEqualTo)
            {
                if (!Object.Equals(value, other))
                    return null;
            }
            else
            {
                // check that both objects are IComparables
                if (!(value is IComparable) || !(other is IComparable))
                    return new ValidationResult(String.Format("{0} and {1} must both implement IComparable", validationContext.DisplayName, OtherProperty));

                // compare the objects
                var result = Comparer.Default.Compare(value, other);

                switch (Criteria)
                {
                    case CompareValues.GreaterThan:
                        if (result > 0)
                            return null;
                        break;
                    case CompareValues.LessThan:
                        if (result < 0)
                            return null;
                        break;
                    case CompareValues.GreatThanOrEqualTo:
                        if (result >= 0)
                            return null;
                        break;
                    case CompareValues.LessThanOrEqualTo:
                        if (result <= 0)
                            return null;
                        break;
                }
            }

            // got this far must mean the items don't meet the comparison criteria
            return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
        }

        /// <summary>
        /// Applies formatting to an error message.
        /// </summary>
        /// <param name="name">The name to include in the error message</param>
        /// <returns></returns>
        public override string FormatErrorMessage(string name)
        {
            return String.Format(CultureInfo.CurrentCulture, base.ErrorMessageString, name, OtherProperty, Criteria.Description());
        }

        /// <summary>
        /// retrieve the object to compare to
        /// </summary>
        /// <returns></returns>
        object GetOther(ValidationContext context)
        {
            return null;
        }
    }

    /// <summary>
    /// Indicates a comparison criteria used by the CompareValues attribute
    /// </summary>
    public enum CompareValues
    {
        [Description("=")]
        EqualTo,
        [Description("!=")]
        NotEqualTo,
        [Description(">")]
        GreaterThan,
        [Description("<")]
        LessThan,
        [Description(">=")]
        GreatThanOrEqualTo,
        [Description("<=")]
        LessThanOrEqualTo
    }

To use this attribute one would then just do:

    /// <summary>
    /// Represents a booking
    /// </summary>
    public class Booking : DomainObject<int>
    {
        /// <summary>
        /// The start of the booking
        /// </summary>
        [Required]
        [CompareValues("End", CompareValues.LessThan)]
        public virtual DateTime? Start { get; set; }

        /// <summary>
        /// The end of the booking
        /// </summary>
        [Required]
        public virtual DateTime? End { get; set; }
    }

LinkButtons, UpdatePanels and IDs

Tuesday, June 2nd, 2009

I have an Repeater control sitting on my page, in the ItemTemplate of which are a couple of Button controls which do various things – delete the item, open the item’s detail pages etc… I think it would be nice to wrap the whole thing in an UpdatePanel to give it that whole AJAX-y feel. All good.

Then the client asks if the buttons can be links instead of buttons. No problem, think I, and go ahead and change the Buttons to LinkButtons. At which point the application stops working. Clicking on the link buttons does a full post back (even though they’re in the UpdatePanel) and their associated methods in the code do not fire. Very strange. I remove the UpdatePanel and the methods fire. All very odd.

Now, I don’t like giving IDs to controls that I don’t reference directly in the code. So as a matter of course I remove IDs from things like Buttons, LinkButtons etc… Which is all fine until you put a LinkButton in an UpdatePanel – where an ID is necessary.

So, if you have a LinkButton within an UpdatePanel and it is doing an unexpected post back and the onclick event is not firing, check that you have an ID on it.