Posts tagged MVC

Putting a feedback button on every page with ASP.NET MVC and JQuery

Feedback button

You’ve probably seen many web sites with the floating ‘feedback’ button down the side. Here’s how to add one to your site using jQuery, jQuery UI and ASP.NET MVC.

First make sure you have jQuery and jQuery UI referenced in your master page view together with the CSS file for whichever jQuery UI theme you have chosen.

We’ll make a few changes to the master page view to add the pop-up feedback form, we’ll add an action on a controller to accept the feedback that is posted, and we’ll need a small amount of CSS.

So, after referencing those javascript files and the theme CSS, the first thing to do is to add the following HTML to the bottom of your master page view:

            <div id="feedbackdialog" style="width:300px; height:300px;text-align:left;">
                <p>Your name and/or email: <br />
                <input type="text" id="feedbackEmail" name="feedbackEmail" size="34" value="<%: this.Model.AccountEmailOrEmpty %>" />
                </p>
                <p>Comment:<br />
                <textarea id="feedbackComment" name="comment" cols="35" rows="5"></textarea></p>
                <br />
                <div id="feedbackResult"></div>
            </div>

Now add this code to your global javascript file that also referenced from your master page view … don’t embed it in the page, go ahead and do the right thing and put it in a .js file so it’s not a burden on every page.

//function for the feedback form
$(document).ready(
    function () {
        /* Create the feedback dialog */

        $("#feedbackdialog").dialog(
        {
            closeOnEscape: true,
            modal: true,
            autoOpen: false,
            resizable: false,
            title: 'Feedback',
            width: 400,
            buttons: { "Send": function () {
                var dlg = $(this);
                $.post("/corporate/suggest",
                        {
                            email: dlg.find("input[name='feedbackEmail']").val(),
                            comment: dlg.find("#feedbackComment").val(),
                            url: document.location.href
                         },
                        function (data) {
                            dlg.dialog('close');
                        }
                );
                $(this).html("<p id='feedBackSending'>Sending</p>").dialog({ buttons: {} });
            }
            }
        });

        $('.contact_us').click(function () {
            $("#feedbackdialog").dialog("open");
            return false;
        });
    });

Next we’ll add the action referenced here, in the example we used the url ‘/corporate/suggest’ so, assuming you have a controller called CorporateController, add the following action to it …

        public ActionResult Suggest (string email, string comment, string url)
        {
            if (!string.IsNullOrWhiteSpace(comment))
            {
                // here we will log the feedback to the database and/or send it in email
            }
            return View();
        }

Create a view for ‘Suggest’, it doesn’t matter what’s in it as we don’t use the result currently.

And, finally we need a bit of CSS for the feedback icon itself:

/* Feedback tab */
#feedbackTab
{
	right:0;
    position:fixed;
    width:32px;
    height:150px;
    top: 150px;
    z-index:1;
}

The feedback button now floats on every page, 150px from the top and it’s glued to the right hand side.

Of course you’ll need your own feedback image, or feel free to borrow the one here:- http://www.signswift.com/images/feedback.png

So with that all in place, click the feedback button and a form like this should appear. Fill the information in and send it to the server. Note how we silently grab the url of the page too so we can see which page they were on when the submitted the feedback.

Feedback Form

ASP.NET MVC SEO – Solution Part 1

In a previous post I explained some of the issues with ASP.NET MVC when trying to implement an SEO-optimized web site.  In this post I’ll begin to explore some possible solutions.

Step 1: Master View – some additions

First let’s make it easy to set the meta description, page title, meta keywords and canonical url by adding the following to the head section of the master view:

<head id="Head1" runat="server">

    <title><%=ViewData["PageTitle"]%></title>  <%-- This gets wrapped here, so it sees a title tag and doesn't emit two --%>
    <%=ViewData["PageDescription"]%>           <%-- These are wrapped elsewhere so they vanish if not set--%>
    <%=ViewData["PageKeywords"]%>              <%-- These are wrapped elsewhere so they vanish if not set--%>
    <%=ViewData["CanonicalUrl"]%>              <%-- These are wrapped elsewhere so they vanish if not set--%>

    <meta name="robots" content="noodp" />  <%--Don't use Open Directory Project descriptions--%>

Note how we are not wrapping the canonical URL, and meta tags around the ViewData here (even though it would be more correct to do so). We do this so that when those tags are not present the entire tag disappears from the page instead of rendering a tag with an empty string in it. For the title tag however, that’s universal so let’s do it ‘properly’.

In tomorrow’s post I’ll show how we can set the Canonical URL using an attribute.

ASP.NET MVC meet SEO; SEO meet ASP.NET MVC

Whilst ASP.NET is clearly the best thing to hit .NET web development in a long-time it seems like the framework itself is somewhat challenged when it comes to SEO.   For starters the concept of a page has all but disappeared – sure you can have a ViewPage but there’s no code associated with it.  And sure, you have ASP.NET Routing so you can do anything you like with routes but the catchall route {Controller}/{Action}/{id} is as much a liability as it is a benefit as it catches things you really didn’t want it to catch and generates routes you really didn’t want to generate all too easily.

Convention over configuration is nice and all that, but sometimes a bit of configuration is necessary to bring your house into order, especially when the convention doesn’t allow things you really want for SEO.

So let’s take a look at all the things we really want to be able to do when creating an SEO friendly web site and see how we can get ASP.NET MVC to handle them.

For SEO we need:-

1. The ability to define a canonical url for a page.  To use that canonical URL whenever we generate a route.  To include that canonical url in the page header to instruct search engines that this is the canonical url for that page.

2. The ability to define multiple alternate URLs for a page.   Plans change and your site changes too but you don’t want 404 errors, you want the user to land on the same page even if you changed the URL to improve its SEO keyword content for example.  Ideally you’d like to 301 redirect these legacy urls but having them at least display the right page and including the canonical url in the header for that page is good enough.

3. The ability to use hyphens in urls.  But since Controllers are classes and Actions are methods and the convention is to use them as parts of the URL this isn’t supported out of the box.

4. The ability to define title, meta description and meta keywords tags for a page in such a way that you can enforce rules around them such as requiring every public page to have a title tag, or ensuring that the length of the title tag is reasonable, or ensuring that your product name is on the end of every title tag.

5. The ability to build a sitemap.xml file that we can submit to Google or Bing containing every URL that we want them to index.

In my next few posts I’ll explain how we can overcome all of these shortcomings of ASP.NET MVC to create a great SEO-friendly web site.

Stay tuned!

ASP.NET MVC 2 and the Ambiguous Match Exception for Action methods with different signatures

Although you can use this technique to allow POST requests to a page avoiding a redirect this is now considered a bad practice from a usability perspective because if the user hits refresh they get the classic browser warning. You normally want to use a Post-Redirect-Get pattern: when you make a POST request, once the request completes you do a redirect so that a GET request is fired. In this way when the user refreshes the page, the last GET request will be executed rather than the POST. You can still use this technique to have two post methods with different parameters but the same name, but why bother if each is going to do a redirect anyway back to the page Action method?

One frustration I have with ASP.NET MVC is that you can’t easily have two actions with the same name but with different parameters, e.g. Index(int a, int b), and Index (int a). If you try this you will get an AmbiguousMatchException because it makes no attempt to match the form values with the method parameters to figure out which method you want to call. Now you can decorate the Index() with [AcceptVerbs(HttpVerbs.Get)] so that it at least will not be competing for ASP.NET MVC’s attention during the form post but your other two index methods will still cause the exception.

Supposed you wanted a page /Home/Index that had two forms on it:-

<%using (Html.BeginForm()) { %>
<%=Html.TextBox(“a”) %>
<input type=”submit” name=”submitOne” title=”click me” />
<%} %>

<%using (Html.BeginForm()) { %>
<%=Html.TextBox(“a”) %>
<%=Html.TextBox(“b”) %>
<input type=”submit” name=”submitTwo” title=”click me” />
<%} %>

What we’d like to do is be able to have three action methods, Index(), Index(Int a) and Index (Int a, Int b).

So let’s define our action methods like that and add a filter attribute to them that will filter methods according to the posted values ignoring any for which there aren’t enough posted values to match the number of parameters or for which the parameter names don’t match.

30 /// <summary>

31 /// Post a single integer back to the form, but don’t allow url /Home/Index/23

32 /// </summary>

33 [ParametersMatch]

34 [AcceptVerbs(HttpVerbs.Post)]

35 public ActionResult Index([FormValue]int a)

36 {

37 ViewData["Message"] = “You supplied one value “ + a ;

38

39 return View();

40 }

41

42 /// <summary>

43 /// Post two integers back to the form OR include two integers in the path

44 /// </summary>

45 [ParametersMatch]

46 [AcceptVerbs(HttpVerbs.Post | HttpVerbs.Get)]

47 public ActionResult Index([FormValue]int a, [FormValue]int b)

48 {

49 ViewData["Message"] = “You supplied two values “ + a + ” “ + b;

50

51 return View();

52 }

And finally, here’s the code that makes that possible: an attribute you can apply to a method parameter to indicate that you want it in the posted form, and an action filter that filters out any action methods that don’t match.

With this in place you can (i) avoid an unnecessary redirect and (ii) have actions with the same name but with different parameters.

Create a new ActionFilter like this …

1 namespace TestApplication.Controllers

2 {

3 using System;

4 using System.Collections.Generic;

5 using System.Collections.ObjectModel;

6 using System.Diagnostics.CodeAnalysis;

7 using System.Linq;

8 using System.Reflection;

9 using System.Web.Mvc.Resources;

10 using System.Web.Mvc;

11 using System.Diagnostics;

12

13

14 /// <summary>

15 /// This attribute can be placed on a parameter of an action method that should be present on the URL in route data

16 /// </summary>

17 [AttributeUsage(AttributeTargets.Parameter, AllowMultiple=false)]

18 public sealed class RouteValueAttribute : Attribute

19 {

20 public RouteValueAttribute() { }

21 }

22

23 /// <summary>

24 /// This attribute can be placed on a parameter of an action method that should be present in FormData

25 /// </summary>

26 [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]

27 public sealed class FormValueAttribute : Attribute

28 {

29 public FormValueAttribute() { }

30 }

31

32

33 /// <summary>

34 /// Parameters Match Attribute allows you to specify that an action is only valid

35 /// if it has the right number of parameters marked [RouteValue] or [FormValue] that match with the form data or route data

36 /// </summary>

37 /// <remarks>

38 /// This attribute allows you to have two actions with the SAME name distinguished by the values they accept according to the

39 /// name of those values.  Does NOT handle complex types and bindings yet but could be easily adapted to do so.

40 /// </remarks>

41 [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]

42 public sealed class ParametersMatchAttribute : ActionMethodSelectorAttribute

43 {

44 public ParametersMatchAttribute() { }

45

46 public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)

47 {

48 // The Route values

49 List<string> requestRouteValuesKeys = controllerContext.RouteData.Values.Where(v => !(v.Key == “controller” || v.Key == “action” || v.Key == “area”)).Select(rv => rv.Key).ToList();

50

51 // The Form values

52 var form = controllerContext.HttpContext.Request.Form;

53 List<string> requestFormValuesKeys = form.AllKeys.ToList();

54

55 // The parameters this method expects

56 var parameters = methodInfo.GetParameters();

57

58 // Parameters from the method that we haven’t matched up against yet

59 var parametersNotMatched = parameters.ToList();

60

61 // each parameter of the method can be marked as a [RouteValue] or [FormValue] or both or nothing

62 foreach (var param in parameters)

63 {

64 string name = param.Name;

65

66 bool isRouteParam = param.GetCustomAttributes(true).Any(a => a is RouteValueAttribute);

67 bool isFormParam = param.GetCustomAttributes(true).Any(a => a is FormValueAttribute);

68

69 if (isRouteParam && requestRouteValuesKeys.Contains(name))

70 {

71 // Route value matches parameter

72 requestRouteValuesKeys.Remove(name);

73 parametersNotMatched.Remove(param);

74 }

75 else if (isFormParam && requestFormValuesKeys.Contains(name))

76 {

77 // Form value matches method parameter

78 requestFormValuesKeys.Remove(name);

79 parametersNotMatched.Remove(param);

80 }

81 else

82 {

83 // methodInfo parameter does not match a route value or a form value

84 Debug.WriteLine(methodInfo + ” failed to match “ + param + ” against either a RouteValue or a FormValue”);

85 return false;

86 }

87 }

88

89 // Having removed all the parameters of the method that are matched by either a route value or a form value

90 // we are now left with all the parameters that do not match and all the route and form values that were not used

91

92 if (parametersNotMatched.Count > 0)

93 {

94 Debug.WriteLine(methodInfo + ” – FAIL: has parameters left over not matched by route or form values”);

95 return false;

96 }

97

98 if (requestRouteValuesKeys.Count > 0)

99 {

100 Debug.WriteLine(methodInfo + ” – FAIL: Request has route values left that aren’t consumed”);

101 return false;

102 }

103

104 if (requestFormValuesKeys.Count > 1)

105 {

106 Debug.WriteLine(methodInfo + ” – FAIL : unmatched form values “ + string.Join(“, “, requestFormValuesKeys.ToArray()));

107 return false;

108 }

109

110 Debug.WriteLine(methodInfo + ” – PASS – unmatched form values “ + string.Join(“, “, requestFormValuesKeys.ToArray()));

111 return true;

112 }

113 }

114 }

Building sitemap.xml for SEO ASP.NET MVC

Creating a sitemap.xml file for Google and other search engines can be accomplished in MVC using a simple ActionResult that returns the appropriate XML blog. The problem however is in generating the list of URLs to go into that sitemap.xml file.

In ASP.NET MVC there is no distinction between an action method that is a page and one that is a service call. To remedy that let’s create an ActionFilterAttribute that can be applied to any method to mark it as a page and to record the URL that we want that page to show up as in our sitemap:

1 /// <summary>

2 /// This attribute indicates that a method is an actual page and gives the data for it

3 /// </summary>

4 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]

5 public class MVCUrlAttribute : ActionFilterAttribute

6 {

7 public string Url { get; private set; }

8

9 public MVCUrlAttribute(string url)

10 {

11 this.Url = url;

12 }

13

14 public override void OnResultExecuting(ResultExecutingContext filterContext)

15 {

16 string fullyQualifiedUrl = filterContext.HttpContext.Request.Url.GetLeftPart(UriPartial.Authority) + this.Url;

17 // We build HTML here because we want the View to be easily able to include it without any conditionals

18 // and because the ASP.NET WebForms view engine sometimes doesn’t subsitute <% in certain head items

19 filterContext.Controller.ViewData["CanonicalUrl"] = @”<link rel=”"canonical”" href=”"” + fullyQualifiedUrl + ” />”;

20 base.OnResultExecuting(filterContext);

21 }

22 }

You may wonder at this point why we can’t just use the Routing table to figure this out. The issue is that multiple routes may map onto one page but we still want it to show up just once in the sitemap otherwise we will get slammed for duplicate content. We also want to be able to mark each page with its canonical URL.

Now we can use reflection to find all of the ‘pages’ in our ASP.NET MVC Application and then build a sitemap.xml file from them.

1 List<string> allPageUrls = new List<string>();

2

3 // Find all the MVC Routes

4 Log.Debug(“*** FINDING ALL MVC ROUTES MARKED FOR INCLUSION IN SITEMAP”);

5 var allControllers = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsSubclassOf(typeof(Controller)));

6 Log.DebugFormat(“Found {0} controllers”, allControllers.Count());

7

8 foreach (var controllerType in allControllers)

9 {

10 var allPublicMethodsOnController = controllerType.GetMethods(BindingFlags.Public | BindingFlags.Instance);

11 Log.DebugFormat(“Found {0} public methods on {1}”, allPublicMethodsOnController.Count(), controllerType.Name);

12

13 foreach (var publicMethod in allPublicMethodsOnController)

14 {

15 var mvcurlattr = publicMethod.GetCustomAttributes(true).OfType<MVCUrlAttribute>().FirstOrDefault();

16 if (mvcurlattr != null)

17 {

18 string url = mvcurlattr.Url;

19 Log.Debug(“Found “ + controllerType.Name + “.” + publicMethod.Name + ” <– “ + url);

20 allPageUrls.Add(url);

21 }

22 }

23 }

Issues combining ASP.NET MVC and ASP.NET Web Forms in the same application

Recently I started migrating an ASP.NET WebForms project to ASP.NET MVC.  Hoping to do this in phases I created a combined project that is both a webforms project and an MVC project.

Routing allows some requests to go to the WebForms pages and some to go to the new MVC pages.

Routing has also enabled SEO friendly URLs for all pages.

Everything seemed to be working great until I added a form to an MVC page and the URL it decided to use for posting the results back was one of the ASPX pages instead of the page it should have been.  The issue was that routes added for the legacy ASPX pages were being picked up as matches for the route that was being requested.

This article was helpful in explaining how to configure routes to avoid this problem http://forums.asp.net/t/1484855.aspx but I still had an issue with the root “/” Route which was being picked for every form I created.  So I changed the root route to this and now all is well.  Of course, changing the home page to an MVC Action would also have solved the issue but I’m not ready to flip it just yet.

Route(“”,

new RouteValueDictionary(new { Controller = “Dummy”, Action = “Dummy” }),

new RouteValueDictionary(new { Controller = “Dummy”, Action = “Dummy” }),

routeHandler);