Export .sdif file to the client’s desktop

One of the user requirements of the swim team website is to send the .sdif file that I blogged about here from the webpage to the user’s browser – or download it to the users file system. Since the file is dynamic and I can’t write to the file system of the web server, I need a way to generate the file and then have a dialog box “Save To” open.

The first step was to add a UI project to the solution that matches the MVC project that the swim team currently users. I love the organization of the initial project – it was very easy and logical to drop in a new Web project:

image

I then opened up the home controller and added a base method and ported the code from the Console UI:

[HttpPost] public ActionResult GetSdifFile() { string fileName = @"C:\Users\Public\PracticeMeetSetup.SD3"; MeetSetupFactory factory = new MeetSetupFactory(); Collection<string> collection = factory.CreateMeetSetUp(72); File.WriteAllLines(fileName, collection); return View(); }

Basically, I need to rip out the disk write code and replace it with something that can go to the browser. Being a traditional ASP.Net guy, I immediately thought of something like this:

public void Guess(string filePath) { try { using (StreamReader sr = new StreamReader(filePath)) { String line; while ((line = sr.ReadLine()) != null) { Response.Write(line + "<br />"); } } } catch (Exception ex) { Response.Write("<p>The file could not be read:"); Response.Write(ex.Message + "</p>"); } }

I then thought “Wait, this is MVC…” so I thought of something like this:

[HttpPost] public ActionResult GetSdifFile() { MeetSetupFactory factory = new MeetSetupFactory(); Collection<string> collection = factory.CreateMeetSetUp(72); return View(collection); }

And the view displaying the contents of the collection:

<h2>GetSdifFile</h2> <% foreach (string _currentString in Model){ %> <%= Html.Encode(_currentString) %> <br /> <% } %>

And here are the results:

image

So it is a start – I guess they could copy/paste the contents of the page. However, the User Case is for them to click a button and get a .sdif file that saves to their file system (via, I assume, a Save Dialog box).

I wrote a new function that returns JSON:

public JsonResult CurrentMeetSdifFile() { MeetSetupFactory factory = new MeetSetupFactory(); Collection<string> collection = factory.CreateMeetSetUp(72); JsonResult result = new JsonResult(); result.Data = collection; result.JsonRequestBehavior = JsonRequestBehavior.AllowGet; return result; }

Since the browser doesn’t know what to do with JSON (I only tested in IE), then you get a save dialog box:

image

Saving that to the desktop, I open in note pad and get the following results:

image

The results are losing their line formatting.

This is the right track – but I have bit more work to do.

My 1st step was to add a parameter of the actual meet that the user wants:

public JsonResult CurrentMeetSdifFile(int meetId) { MeetSetupFactory factory = new MeetSetupFactory(); Collection<string> collection = factory.CreateMeetSetUp(meetId); Etc… }

I then tried it from the browser:

image

Ugh, it looks like with the default routing engine, I need to make the parameter have the name id. I have a choice. I can either add a new route with an explicit meetId or I can use the id variable name. I chose option B as a path of least resistance:

public JsonResult CurrentMeetSdifFile(int id) { MeetSetupFactory factory = new MeetSetupFactory(); Collection<string> collection = factory.CreateMeetSetUp(id); JsonResult result = new JsonResult(); result.Data = collection; result.JsonRequestBehavior = JsonRequestBehavior.AllowGet; return result; }

With the routing set up, I now need to fix the JSON output to stick a new line after each string in the collection.

My 1st attempt was just to throw an Environment.NewLine into the Json result:

image

Yikes! It looks like I am mixing formatting. “\r\n” is coming down as a literal value. I need a way to tell notepad that they are line breaks.

I binged around a bit and looked at attack overflow. This post seems to have the answer. I tried to add

result.ContentType = "text/html";

But then the browser displayed the data:

image

Note the “\r\n” is still there.

I then tried some other ways of breaking (using the \\n for example), nothing.

I then stepped back and thought that I was approaching the problem incorrectly. Instead of sending back JSON from the function, what if I sent something else? A quick tour through MSDN showed me a file result class. That is what I need and here is an example of my question.

A quick run through the overloads of the method, I realized that all I have to do is to convert the string collection into a FileStream and then send it out via the File class. I used the FileStreamResult class and the rest was pretty easy to wire up:

public FileStreamResult CurrentMeetSdifFile(int id) { MeetSetupFactory factory = new MeetSetupFactory(); Collection<string> collection = factory.CreateMeetSetUp(id); StringBuilder stringBuilder = new StringBuilder(); foreach(string _currentString in collection) { stringBuilder.Append(_currentString); stringBuilder.Append(Environment.NewLine); } byte[] byteArray = Encoding.ASCII.GetBytes(stringBuilder.ToString()); MemoryStream stream = new MemoryStream( byteArray ); FileStreamResult fileStreamResult = new FileStreamResult(stream,"text/plain"); return fileStreamResult; }

And if I want to get the popup, I change the output format to “.sdif” and I get the dialog box with the data formatted correctly.

image

And boom goes the dynamite…

Advertisements

MVC Route Constraints

I started to dig into MVC3 routing a bit more over the weekend.  I came across some routing constraints and realized that there is a clear progression.  I created an out of the box MVC3 web application (I am using Razor) and then added a Product Controller with the default methods and 1 View for the Details method.  I made 1 change to the out of the box convention – I changed the name of the int parameter to the Details method to productId.

 

image

The Details controller method pushes the parameter back out to the View:

public ActionResult Details(int productId) { return View(productId); }

and the view parrots the productId back to the user:

@{ ViewBag.Title = "Details"; Layout = "~/Views/Shared/_Layout.cshtml"; } <h2>Details</h2> <br /> You Entered = @Model

 

I then dropped into Global.asax to work with the constraints.  The first thing I did was to add a route to the routing table to account for the productid parameter:

routes.MapRoute( "Product", "Product/{productId}", new { controller = "Product", action = "Details" } );

This worked fine to a point.  Product/1 resolved:

image

 

but Product/foo was allowed by the routing engine and the method threw an error:

 

image

 

To handle this malformed parameter, I added a regular expression to the constraint definition (using Stephen Walter’s blog post as an example:

 

routes.MapRoute( "ProductWithConstraint", "Product/{productId}", new { controller = "Product", action = "Details" }, new { productId = @"\d+" } );

Now, when Product/foo comes in, I got a 404.

image

The next scenario I wanted to handle was that only some integers are allowed as a parameter.  For example, perhaps only products that are in stock are allowed – which means that you need to do a database call before defining the route constraint definition.  To that end, I created a new class that inherited from IRouteConstraint (based on Yuri Nayyeri blog post):

public class ProductRouteConstraint: IRouteConstraint { public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { if ((routeDirection == RouteDirection.IncomingRequest) && (parameterName.ToLower(CultureInfo.InvariantCulture) == "productid")) { int productId = 0; try { productId = Convert.ToInt32(values["productId"]); if (productId > 0 && productId < 10) { return true; } } catch (FormatException formatException) { return false; } } return false; } }

I then added this constraint to the routing table:

 

routes.MapRoute( "ProductWithConstraintClass", "Product/{productId}", new { controller = "Product", action = "Details" }, new { productId = new ProductRouteConstraint() } );

And I got the desired result – products less than 0 or greater than 10 returned a 404.

So the progression in my mind is:

  1. No constraints
  2. Reg Ex constraints in the Global.asax
  3. Create a class that implements IRouteConstraint

I my mind, I would rather skip #2 and go to #3 altogether.  Having all of the logic encapsulated in 1 class seems cleaner and not having the Global.asax mucked up with custom defs makes for a more maintainable solution.

Search Engine Optimization Toolkit For IIS

I wanted to make MPMS Girls lax page more Search Engine friendly. I have never done any SEO ever so I jumped into Scott Allen’s article about SEO.

The first thing I did was to download the SEO toolkit to give myself a baseline. I launched the Search Engine Optimization (SEO) Toolkit and pointed to www.mpmslax.com:

image

And by looking at the detail:

image

Working backwards:

Issue #7 has the blog redirect is throwing things off. Since we don’t even use that blog, I dropped it from the site.

Issue #6 – multiple <h1> was easily solved, I went to the offending .aspx page and dropped the <h1>. There is 1 <h1> and it is in the master page.

Issue #5 – canonical formats – I will deal with in a bit.

Issue #4 – not ALT attribute – all on the sponsor page – I just added a AlternateText attribute to the markup

Issue #3 – Title is empty – fixed (see below)

Issue #2 – Dropping the blog solves this problem

Issue #1 – Description is empty – fixed (See below)

The first couple of recommendations from Scott Allen and picked up by the SEO were easy. I added a title and some meta tags to my master page:

<head runat="server"> <title>Mills Park Girls Lacrosse</title> <meta name="keywords" content="MPMS, Mills Park Middle School, Girls Lacrosse, Girls Lax" /> <meta name="description" content="Mills Park Girls Lacrosse" /> <link href="~/Styles/Site.css" rel="stylesheet" type="text/css" /> <asp:ContentPlaceHolder ID="HeadContent" runat="server"> </asp:ContentPlaceHolder> </head>

I deployed the site and re-ran the SEO and got only the canonical formats error. Interestingly, I don’t see where the error is coming from. For example, I have these 2 errors:

The page with URL "http://www.mpmslax.com/girls/Public/ContactUs.aspx&quot; can also be accessed by using URL "http://www.mpmslax.com/girls/Public/Coaches.aspx&quot;.

The page with URL "http://www.mpmslax.com/girls/Public/Coaches.aspx&quot; can also be accessed by using URL "http://www.mpmslax.com/girls/Public/ContactUs.aspx&quot;.

But here is the relevant part of the site map:

<siteMapNode url="~/Public/Coaches.aspx" title="Coaches" description="MS Girls Lax Coaches" /> <siteMapNode url="~/Public/ContactUs.aspx" title="Contact Us" description="MP Girls Lax Contact Us" />

That’s it – I have no idea where this error is coming from.

On a related canonical issue, I have the following urls for the site:

· www.mpmslax.com

· www.mpmslax.org

· www.mpmslacrosse.com

· www.mpmslacrosse.org

Since WInHost does not point at subdirectories, I am stuck unless I write a url redirect script. Taking the path of least resistance, I just popped in a basic MVC site to the main directory. People can then click on the link to go to the girls subdirectory.

I finally wrapped up URL Rewriter – it was already installed on WinHost. I can’t believe how easy it is – Just open the dialog and select the rule you want to enforce:

image

I picked all 3 SEO ones and called it a day. I am really impressed with the IIS add-ins – they really made my life easier.

Overloading Controller Methods in a MVC Project

I cranked up a new MVC3 project (Razor and Unit Testing). I went to the Home controller and added an overloaded method to the Index:

public ActionResult Index(string userName) { ViewBag.Message = String.Format("Welcome to ASP.NET MVC, {0}!", userName); return View(); }

I then hit F5 and got the following error:

image

 

I tried adding a route:

routes.MapRoute( "HomeIndexWithString", "Home/Index/{userName}", new { controller = "Home", action = "Index", userName = "" } );

and being explicit with the URL but I got the same error:

image

I then looked on Stack Overflow and added the ActionName attribute to my overloaded method:

[ActionName("HomeIndexWithString")] public ActionResult Index(string userName) { ViewBag.Message = String.Format("Welcome to ASP.NET MVC, {0}!", userName); return View(); }

The Index page returned, but the problem is that even when I passed in the explicit url, it still returned the default page. I then went back to the overloaded method and deleted it. I then added an optional parameter to the original index method and handled it if the value was populated:

public ActionResult Index(string? userName) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("Welcome to ASP.NET MVC"); if (userName != null) { stringBuilder.Append(String.Format(", {0}!", userName)); } ViewBag.Message = stringBuilder.ToString(); return View(); }

The problem is that the compiler complains:

Error 1 The type ‘string’ must be a non-nullable value type in order to use it as parameter ‘T’ in the generic type or method ‘System.Nullable<T>’

So I am down to creating a custom ActionMethodSelectAttribute that can handle the requests and parse appropriately. Since this smells like a kludge, I went back to the way MSFT wants me to do it – create a different method for each action and return to appropriate view. Naming confusion notwithstanding, I guess that is what I will do…

MVC, LINQ, and WinHost

I love MVC. I recently had to make some changes to the swim team website to get ready for the 2011 season. Because there was a nice SOC enforced by MVC, I simply had to add a couple of new controllers with some basic CRUD:

clip_image001

add their associated views, and then wire up to my factory using my POCOs:

public ActionResult Index(int? page) { FamilyFactory familyFactory = new FamilyFactory(); IQueryable<Family> families = familyFactory.GetAllFamilies() .OrderBy(family => family.FamilyDesc) .AsQueryable <Family>(); const int pageSize = 10; var paginatedFamilies = new PaginatedList<Family>(families, page ?? 0, pageSize); return View(paginatedFamilies); }

(note that I used the paginated list from Nerd Dinner)

Boom goes the dynamite.

Upon reflection, you can really see how useful using POCOs are when you have to extend your application. That upfront cost 2 years ago is paying huge dividends now.

I did run into 1 small hiccup when I deployed to WinHost. I received the following error:

clip_image002

After some digging (this error does not Google well), I added this:

<trust level="Full" />

To System.Web and things corrected.