Showing posts with label MVC. Show all posts
Showing posts with label MVC. Show all posts

2008-06-11

Returning a binary respose in ASP MVC RC3

In a previous post I showed how to return a binary file from a controller action, well, this no longer works in release candidate 3 of the framework. Instead you have to create a new ActionResult descendant to do the job for you. This is how I did it....

return new BinaryResult(data, Path.GetFileName(productFileName));


and the class is implemented like so:

public class BinaryResult : ActionResult
{
private string ClientFileName;
private byte[] Data;
private string VirtualFileName;

public BinaryResult(string virtualFileName, string clientFileName)
{
if (string.IsNullOrEmpty(virtualFileName))
throw new ArgumentNullException("VirtualFileName");
if (string.IsNullOrEmpty(clientFileName))
throw new ArgumentNullException("ClientFileName");

ClientFileName = clientFileName;
VirtualFileName = virtualFileName;
}

public BinaryResult(byte[] data, string clientFileName)
{
if (data == null)
throw new ArgumentNullException("Data");
if (string.IsNullOrEmpty(clientFileName))
throw new ArgumentNullException("ClientFileName");

ClientFileName = clientFileName;
Data = data;
}

public override void ExecuteResult(ControllerContext context)
{
if (!string.IsNullOrEmpty(VirtualFileName))
{
string localFileName = context.HttpContext.Server.MapPath(VirtualFileName);
FileStream fileStream = new FileStream(localFileName, FileMode.Open, FileAccess.Read, FileShare.Read);
using (fileStream)
{
Data = new byte[fileStream.Length];
fileStream.Read(Data, 0, (int)fileStream.Length);
}//using fileStream
}

context.HttpContext.Response.AddHeader("content-disposition", "attachment; filename=" + ClientFileName);
context.HttpContext.Response.BinaryWrite(Data);
}
}

2008-05-29

ASP MVC preview 3 released

I'm trying to upgrade from Preview 2 to Preview 3. I think the idea of having each action return an ActionResult was a good one, so far it has actually made my code slightly smaller.

What I don't understand though is why Html.Select seems to have disappeared...

Compilation Error
Description: An error occurred during the compilation of a resource required to service this request. Please review the following specific error details and modify your source code appropriately.

Compiler Error Message: CS1501: No overload for method 'Select' takes '5' arguments

Source Error:

Line 8: Product
Line 9:
Line 10: <%= Html.Select("SoftwareID", (object)ViewData["SoftwareList"], "Name", "ID", (object)ViewData["SoftwareID"]) %>
Line 11:
Line 12:


When I go into the APX and type Html. there is no Select method listed along with the other options! Where is it?

2008-04-04

Binary response in ASP MVC

Today I wanted to give access to certain files on a website only via my DownloadController. This was so that I could ensure the current user had purchased the item in question first, and also sign any license info into the download aswell.



I tried getting a URL like this to work



http://localhost/download/1/SomeFileName



which would remap to the DownloadController



public void Index(int id, string fileName)





This worked fine, and because the URL ended with "SomeFileName" it would get saved as the correct filename too, but this was no use because SomeFileName has no file extension. As soon as I added .zip on the end the request no longer went via the new HttpHandler in the MVC web extensions. Even when I added it in the <httpHandlers> section of web.config it just wouldn’t work.



My problem was in relying on the url for the filename. This is apprarently not the way it should be done. Instead I should have stuck to the standard URL approach



http://localhost/download/1



and added a special HTTP header known as "content-disposition" to the response, this tells the client what the filename should be. Here is a full example of how to write a binary file to the Response when using the new MVC ASP Web Extensions, and how to have it saved on the client with the correct filename.



public void Index(int id)
{
 IProductRepository productRepository = EcoSpace.GetEcoService<IProductRepository>();
 Product product = productRepository.GetByID(id);
 if (product == null)
 {
  ViewData[GlobalViewDataKeys.ErrorMessage] = "Item not found";
  Response.Redirect("/Account/Home", false);
  return;
 }

 Response.ContentType = "Application/" + Path.GetExtension(product.DownloadUrl).Substring(1);
 Response.AppendHeader("content-disposition", "inline; filename=" + product.DownloadUrl);

 string localFileName = "";
 if (product is Edition)
  localFileName = FilePathUrls.Software;
 else
  if (product is Collateral)
   localFileName = FilePathUrls.Collateral;
  else
   throw new NotImplementedException(product.GetType().Name);

 localFileName = Request.MapPath(localFileName);
 localFileName = Path.Combine(localFileName, product.DownloadUrl);

 FileStream fileStream = new FileStream(localFileName, FileMode.Open, FileAccess.Read, FileShare.Read);
 byte[] data = new byte[fileStream.Length];
 using (fileStream)
  fileStream.Read(data, 0, (int)fileStream.Length);
 Response.BinaryWrite(data);
 Response.End();
}



Thanks go to Phil Haak who pointed me in the right direction and was kind enough to promptly help a complete stranger!

2008-03-07

ECO, LINQ, Anonymous types, and Web Extensions

I’ve been finding LINQ + Anonymous types really compliment ECO and the new ASP web extensions approach to writing websites. I may have mentioned recently that I don’t like the idea of passing instances of my business objects to the presentation layer. The reason is that someone else will be writing the views for this site and I want to be able to control what they are capable of displaying. It’s not just that though, the fact is that your view might need to look completely different to how your business classes are structured, one layer should not dictate the structure of another.

The example I am about to show does in fact have similar structures for the view and model. Having said that there is a slight difference in that the MinorVersion class has its own "int VersionNumber" property, and gets the major part of the version number from self.MajorVersion.VersionNumber. Anyway, now to get on with it.

My requirement was to show all major versions, within each major version show each minor version, and within each minor version show a list of what’s new. In addition, a minor version should only be displayed if its status is #Released, and a major version should not be displayed if it has no minor versions which meet this criteria.


The following code generates a structure like so

MajorVersion (VersionNumber)
 1..* MinorVersion (MajorVersionNumber, MinorVersionNumber)
  1..* WhatsNew (ID, Headline)



and stores the resulting anonymous type into the ViewData for my view to render.

ViewData[GlobalViewDataKeys.WhatsNewKeys.WhatsNewList] = 
 from majorVersion in software.MajorVersions
 where
  (from minorVersionCheck in majorVersion.MinorVersions
   where minorVersionCheck.Status == MinorVersionStatus.Released select minorVersionCheck ).Count() > 0
 select
  new
  {
   VersionNumber = majorVersion.VersionNumber,
   MinorVersions =
    from minorVersion in majorVersion.MinorVersions
    select
     new
     {
      MajorVersionNumber = majorVersion.VersionNumber,
      VersionNumber = minorVersion.VersionNumber,
      WhatsNew =
       from whatsNew in minorVersion.WhatsNew
       select
        new
        {
         ID = whatsNew.ID,
         Headline = whatsNew.Headline
        }
     }
  };
RenderView("AllHistory");




The code behind of my view reads like this:

protected void Page_Load(object sender, EventArgs e)
{
 MajorVersionRepeater.DataSource = ViewData[GlobalViewDataKeys.WhatsNewKeys.WhatsNewList];
 MajorVersionRepeater.DataBind();
}



And finally I use nested ASP:Repeater tags to render the nested HTML.


<ul class="AllHistoryMajorVersionList">
 <asp:Repeater id="MajorVersionRepeater" runat="server">
  <ItemTemplate>
   <li>
    Major version
     <%# DataBinder.Eval(Container.DataItem, "VersionNumber") %>
    <ul class="AllHistoryMinorVersionList">
     <asp:Repeater
      id="MinorVersionRepeater"
      DataSource=’<%# DataBinder.Eval(Container.DataItem, "MinorVersions") %>’
      runat="server">
      <ItemTemplate>
       <li>
        Minor version
        <%# DataBinder.Eval(Container.DataItem, "MajorVersionNumber") %>.
        <%# DataBinder.Eval(Container.DataItem, "VersionNumber") %>
        <ul class="AllWhatsNewList">
         <asp:Repeater
          id="WhatsNewRepeater"
          DataSource=’<%# DataBinder.Eval(Container.DataItem, "WhatsNew") %>’
          runat="server">
          <ItemTemplate>
           <li>
            <a href="/WhatsNew/View/<%# DataBinder.Eval(Container.DataItem, "ID") %>">
             <%# DataBinder.Eval(Container.DataItem, "Headline") %>
            </a>
           </li>
          </ItemTemplate>
         </asp:Repeater>
        </ul>
       </li>
      </ItemTemplate>
     </asp:Repeater>
    </ul>
   </li>
  </ItemTemplate>
 </asp:Repeater>
</ul>



I think the point is that there is just no need to pass your business class instances through to the UI layer. In fact if you later changed the structure of your business classes this LINQ would no longer compile, whereas the markup in the view is evaluated at runtime so you wouldn’t spot an error here until you tried to view the page.