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-05-28

Hooking into ECO multi-association events

Although I have not (yet) needed this myself I can see myself needing it in the future and the question has been asked before.

"Setting HasUserCode=True on a Child.Parent single role does what I want, but how do I handle the scenario where Parent.Children.Add(item) is called on a multirole?"

By default you can’t, but with the addition of a single class and a small amount of tweaking you can get it to do what you want! Here is how to do it:


01: Mark Parent.Children’s association end with HasUserCode=True in the modeler and then generate code.
02: In the source code of your class (not within an ECO region) add the following


  private EcoMultiAssociation<Child> m_Children;


This is a class that does not yet exist, I will show the source code for it later.


02: In the source code locate the "Children" property and change it like so

  public IEcoList<Child> Children
  {
    get
    {
      if (m_Children == null)
      {
        m_Children= new EcoMultiAssociation<Child>((IList)(this.eco_Content.get_MemberByIndex(Eco_LoopbackIndices.Children_MemberIndex)));
        m_Children+= new AssociationItemChangedEventHandler<Child>(result_ItemChanged);
        m_Children+= new AssociationChangedEventHandler<Child>(result_ItemInserted);
        m_Children+= new AssociationChangedEventHandler<Child>(result_ItemRemoved);
      }
      return m_Children

#if NeverDoThis
      #region MM_ECO_Generated
      return new ObjectListAdapter<Child>((IList) (this.eco_Content.get_MemberByIndex(Eco_LoopbackIndices.Children_MemberIndex)));
      #endregion
#endif
    }
  }


Note that I have put an #if around the original code that will never be true. You cannot remove the MM_ECO_Generated section due to the source code generator expecting to find it, but you can make sure it is never even compiled! The parameters for the EcoMultiAssociation<T> constructor were just copied directly from the ObjectListAdapter constructor below.

(Just a small note, I don’t usually name my private members m_Name, I just did it in this case to make it easier to spot the difference between the Children property and the m_Children private member).


03: The event handlers are implemented like so

    void result_ItemRemoved(object sender, AssociationChangedEventArgs<Child> args)
    {
      System.Diagnostics.Debug.WriteLine(string.Format("Removed index {0}", args.Index));
    }

    void result_ItemInserted(object sender, AssociationChangedEventArgs<Child> args)
    {
      System.Diagnostics.Debug.WriteLine(string.Format("Inserted at index {0}", args.Index));
    }

    void result_ItemChanged(object sender, AssociationItemChangedEventArgs<Child> args)
    {
      System.Diagnostics.Debug.WriteLine(string.Format("Changed object at index {0}", args.Index));
    }


The args parameter has a reference to the old object and also the new object in the case of ItemChanged which is executed when you do this...

Association[x] = y;



Finally here is the source code for the EcoMultiAssociation<T> class. There’s quite a bit here, but that’s really because I have to implement so many interfaces, the actual code is very small.

  public delegate void AssociationChangedEventHandler<T>(object sender, AssociationChangedEventArgs<T> args);
  public class AssociationChangedEventArgs<T> : EventArgs
  {
    public readonly T Item;
    public readonly int Index;

    public AssociationChangedEventArgs(int index, T item)
    {
      Item = item;
      Index = index;
    }
  }

  public delegate void AssociationItemChangedEventHandler<T>(object sender, AssociationItemChangedEventArgs<T> args);
  public class AssociationItemChangedEventArgs<T> : AssociationChangedEventArgs<T>
  {
    public readonly T OriginalItem;

    public AssociationItemChangedEventArgs(int index, T newItem, T originalItem)
      : base(index, newItem)
    {
      OriginalItem = originalItem;
    }
  }


  public class EcoMultiAssociation<T> : IEcoList<T>, IList
  {
    private readonly IList Adaptee;

    public EcoMultiAssociation(IList adaptee)
    {
      if (adaptee == null)
        throw new ArgumentNullException("Adaptee");

      Adaptee = adaptee;
    }

    public void Add(T item)
    {
      if (Adaptee.IndexOf(item) == -1)
      {
        Adaptee.Add(item);
        OnItemInserted(Count - 1, item);
      }
    }

    public void Clear()
    {
      List<T> originals = new List<T>(this);
      Adaptee.Clear();
      for (int index = 0; index < originals.Count; index++)
        OnItemRemoved(index, originals[index]);
    }

    public bool Contains(T item)
    {
      return Adaptee.Contains(item);
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
      Adaptee.CopyTo(array, arrayIndex);
    }

    public int Count
    {
      get { return Adaptee.Count; }
    }

    public IEnumerator<T> GetEnumerator()
    {
      return new ObjectEnumeratorAdapter<T>(Adaptee.GetEnumerator());
    }

    public int IndexOf(T item)
    {
      return Adaptee.IndexOf(item);
    }

    public void Insert(int index, T item)
    {
      if (Adaptee.IndexOf(item) == -1)
      {
        Adaptee.Insert(index, item);
        OnItemInserted(index, item);
      }
    }

    public bool IsReadOnly
    {
      get { return Adaptee.IsReadOnly; }
    }

    public bool Remove(T item)
    {
      int index = Adaptee.IndexOf(item);
      if (index == -1)
        return false;

      Adaptee.RemoveAt(index);
      return true;
    }

    public void RemoveAt(int index)
    {
      T item = this[index];
      Adaptee.RemoveAt(index);
      OnItemRemoved(index, item);
    }

    public T this[int index]
    {
      get
      {
        return (T)Adaptee[index];
      }
      set
      {
        T originalItem = (T)Adaptee[index];
        Adaptee[index] = value;
        OnItemChanged(index, originalItem, value);
      }
    }

    public event AssociationChangedEventHandler<T> ItemInserted;
    protected void OnItemInserted(int index, T item)
    {
      AssociationChangedEventHandler<T> handler = ItemInserted;
      if (handler != null)
      {
        AssociationChangedEventArgs<T> args = new AssociationChangedEventArgs<T>(index, item);
        handler(this, args);
      }
    }

    public event AssociationChangedEventHandler<T> ItemRemoved;
    protected void OnItemRemoved(int index, T item)
    {
      AssociationChangedEventHandler<T> handler = ItemRemoved;
      if (handler != null)
      {
        AssociationChangedEventArgs<T> args = new AssociationChangedEventArgs<T>(index, item);
        handler(this, args);
      }
    }

    public event AssociationItemChangedEventHandler<T> ItemChanged;
    protected void OnItemChanged(int index, T originalItem, T newItem)
    {
      AssociationItemChangedEventHandler<T> handler = ItemChanged;
      if (handler != null)
      {
        AssociationItemChangedEventArgs<T> args = new AssociationItemChangedEventArgs<T>(index, newItem, originalItem);
        handler(this, args);
      }
    }


    #region IEnumerable Members

    IEnumerator IEnumerable.GetEnumerator()
    {
      return GetEnumerator();
    }

    #endregion

    #region IList Members

    int IList.Add(object value)
    {
      Add((T)value);
      return IndexOf((T)value);
    }

    bool IList.Contains(object value)
    {
      return Contains((T)value);
    }

    int IList.IndexOf(object value)
    {
      return IndexOf((T)value);
    }

    void IList.Insert(int index, object value)
    {
      Insert(index, (T)value);
    }

    bool IList.IsFixedSize
    {
      get { return Adaptee.IsFixedSize; }
    }

    void IList.Remove(object value)
    {
      Remove((T)value);
    }

    object IList.this[int index]
    {
      get
      {
        return this[index];
      }
      set
      {
        this[index] = (T)value;
      }
    }

    #endregion

    #region ICollection Members

    void ICollection.CopyTo(Array array, int index)
    {
      Adaptee.CopyTo(array, index);
    }

    bool ICollection.IsSynchronized
    {
      get { return Adaptee.IsSynchronized; }
    }

    object ICollection.SyncRoot
    {
      get { return Adaptee.SyncRoot; }
    }

    #endregion

  }



Note that these events will not be executed if you do Child.Parent = p; For this case you have to set HasUserCode=True on Child.Parent as normal and react accordingly.

2008-05-08

Using Vista Aero theme in XP WPF apps

I found this article on the web recently which shows how to use the Vista Aero theme on XP in your WPF apps.

I found two things:
01: It is less complicated that the article states.
02: It is a bit different if you already have stuff in your app resources, such as styles or control templates etc.

So here are my steps
01: Add PresentationFramework.Aero to your applications References list. It is listed in the [.NET] tab.
02: Edit your App.xaml and change it from this

<Application.Resources>
  <!-- Your stuff here -->
</Application.Resources>

to this

<Application.Resources>
  <ResourceDictionary>
    <!-- Put your stuff here instead -->

    <ResourceDictionary.MergedDictionaries>
      <ResourceDictionary Source="/PresentationFramework.Aero;component/themes/aero.normalcolor.xaml"/>
    </ResourceDictionary.MergedDictionaries>
  </ResourceDictionary>
</Application.Resources>

2008-05-04

Selecting WPF ListView row in code

An app I am considering migrating to WPF uses a DevExpress grid to show a list of events, as I animate my composition I highlight the current event in the grid. Things I have learned about WPF:

01: You can create a data grid type view like so:
<ListView Name="ListView">
  <ListView.View>
    <GridView AllowsColumnReorder="False">
      <GridViewColumn DisplayMemberBinding="{Binding Path=Title}" Header="Title"/>
      <GridViewColumn DisplayMemberBinding="{Binding Path=FirstName}" Header="FirstName"/>
      <GridViewColumn DisplayMemberBinding="{Binding Path=LastName}" Header="LastName"/>
    </GridView>
  </ListView.View>
</ListView>


02: Don’t put a ListView in a <StackPanel>! It renders all of the data at once. When you have 6K rows of data for example it takes about 1 second to select the next row. Instead you should put it in a grid

<Grid>
  <Grid.RowDefinitions>
    <RowDefinition Height="50"/>
    <RowDefinition Height="*"/>
  </Grid.RowDefinitions>

  <Button Grid.Row="0"...../>
  <ListView Grid.Row="1"...../>
</Grid>

03: When selecting a row in code the row does not become visible. To automatically make the row visible you need to use ScrollIntoView()

  ListView.SelectedIndex++;
  ListView.ScrollIntoView(ListView.SelectedItem);

2008-05-01

Creating a drop shadow

Requirement: Take a PNG image that has an alpha mask and from it generate a new bitmap which has the same alpha mask but every pixel is black.

Solution:
private Bitmap GenerateDropShadowImage(Image image)
{
  Bitmap bitmap = new Bitmap(image);
  BitmapData bits = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), System.Drawing.Imaging.ImageLockMode.ReadWrite, System.Drawing.Imaging.PixelFormat.Format32bppArgb);

  IntPtr bottomScanLine = bits.Scan0;
  int bytesPerRow = bits.Width * 4;

  unsafe
  {
    byte* pixelValue = (byte*)bottomScanLine.ToPointer();
    for (int count = 0; count < bits.Width * bits.Height; count++)
    {
      pixelValue[0] = 0;
      pixelValue[1] = 0;
      pixelValue[2] = 0;
      pixelValue = pixelValue + 4;
    }
  }
  bitmap.UnlockBits(bits);
  return bitmap;
}

2008-04-25

WPF custom button templates


WPF Button

I’ve had a bit of a play with WPF for the first time today. I decided to create my own button template. The idea of a template is that you can redefine the visual elements that make up the control.

Create a new WPF application

Now add a button within the grid like so

 <Grid>
  <Button Content="Click me" Width="150" Height="50"/>
 </Grid>

So that we can see what we are designing add a gradient background to the window. Within the <Window> node add

 <Window.Background>
  <LinearGradientBrush>
   <GradientStop Color="Black" Offset="0"/>
   <GradientStop Color="White" Offset="1"/>
  </LinearGradientBrush>
 </Window.Background>


Now to start designing the button template. Within the <Window> node add
 <Window.Resources>
 </Window.Resources>

this is where we will add the template, within that new node add the following

 <ControlTemplate x:Key="PetesButton" TargetType="{x:Type Button}">
 </ControlTemplate>

and then change the button to use this template

 <Button Content="Click me" Width="150" Height="50" Template="{DynamicResource PetesButton}"/>


This creates a control template that targets the "Button" control. As a result the button will disappear, this is because our button template is currently empty. To add a rounded rectangle as the outline for the button by adding a <Border> element.

 <Border BorderThickness="1,1,1,1" CornerRadius="4,4,4,4" BorderBrush="Black" Background="Black">
 </Border>

Next a <Grid> will be added within that new <Border> element, this is because by default a grid will stack controls on top of each other in a Z order rather than laying them out vertically or horizontally. Within the <Grid> add another border which will act as the client area of the control

 <Grid>
  <Border x:Name="BorderUp" BorderThickness="2,2,2,2" CornerRadius="4,4,4,4" Background="White">
  </Border>
 </Grid>

Next add a nice gradient to this border like so
 
 <Border.BorderBrush>
  <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1" >
   <GradientStop Color="White" Offset="0"/>
   <GradientStop Color="#222222" Offset="1"/>
  </LinearGradientBrush>
 </Border.BorderBrush>

This sets the BorderBrush property for the Border to a LinearGradient which starts at 0.5,0 (X=50%, Y=0%) and ends at 0.5,1 (X=50%, Y=100%). The gradient is made up of two parts, White at offset 0 (the very beginning of the gradient) and #222222 at offset 1 (the very end of the gradient).

As this gradient looks so nice lets remove the Background="White" from the <Border> and inside it add a LinearGradient to the background

 <Grid>
  <Border x:Name="BorderUp" BorderThickness="2,2,2,2" CornerRadius="4,4,4,4">
   <Border.Background>
    <LinearGradientBrush StartPoint=".5,0" EndPoint=".5,1">
     <GradientStop Color="#aaaaff" Offset="0"/>
     <GradientStop Color="#444466" Offset="0.6"/>
     <GradientStop Color="#444444" Offset="1"/>
    </LinearGradientBrush>
   </Border.Background>
  </Border>
 </Grid>


This does exactly the same as the BorderBrush, except there is an addition colour at offset 0.6 (60% of the way along the gradient).

Now we have a pretty looking button, but where is the text? To show the text we need a <ContentPresenter>. Beneath the "BorderUp" element (and within <Grid>) add the following

 <ContentPresenter x:Name="Contents" HorizontalAlignment="Center" VerticalAlignment="Center" Width="Auto" Margin="3,3,3,3"/>

Now you will see the text appear! Run the app, the button looks nice! Click the button, now it looks rubbish! :-)

Below the "BorderUp" element and above the "Contents" element add another border that is the reverse of BorderUp, by this I mean that the colours are switched in the gradient, name it "BorderDown"

 <Border x:Name="BorderDown" BorderThickness="2,2,2,2" CornerRadius="4,4,4,4">
  <Border.BorderBrush>
   <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1" >
    <GradientStop Color="White" Offset="1"/>
    <GradientStop Color="#222222" Offset="0"/>
   </LinearGradientBrush>
  </Border.BorderBrush>
  <Border.Background>
   <LinearGradientBrush StartPoint=".5,0" EndPoint=".5,1">
    <GradientStop Color="#aaaaff" Offset="0"/>
    <GradientStop Color="#444466" Offset="0.6"/>
    <GradientStop Color="#444444" Offset="1"/>
   </LinearGradientBrush>
  </Border.Background>
 </Border>

The button now looks like it is sunk into the form. Change the BorderDown element so that it has no opacity

 <Border x:Name="BorderDown" BorderThickness="2,2,2,2" CornerRadius="4,4,4,4" Opacity="0">

Now to make this sunken border visible when the mouse is pressed down. Just before the closing </ControlTemplate> node add


Within that node add a trigger that reacts to the IsPressed property changing

 <ControlTemplate.Triggers>
  <Trigger Property="IsPressed" Value="True">
   <Setter TargetName="BorderDown" Property="Opacity" Value="1"/>
   <Setter TargetName="Contents" Property="Margin" Value="4,4,2,2"/>
  </Trigger>
 </ControlTemplate.Triggers>


Now try running the app and clicking the button. Much nicer, but I think we can improve on it some more!


Above the </ControlTemplate.Triggers> closing node add

 <ControlTemplate.Resources>
 </ControlTemplate.Resources>

Within this section add a <StoryBoard> node with the name "MouseDownTimeLine"

 <Storyboard x:Name="ButtonDownTimeLine">
 </StoryBoard>

Now we are going to animate a property of type Double, to do this we need a DoubleAnimationUsingKeyFrames node

 <Storyboard x:Key="ButtonDownTimeLine">
  <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity">
   <SplineDoubleKeyFrame KeyTime="00:00:00.05" Value="1"/>
  </DoubleAnimationUsingKeyFrames>
 </Storyboard>

and also the opposite, an animation setting the opacity of BorderDown back to zero

 <Storyboard x:Key="ButtonUpTimeLine">
  <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity">
   <SplineDoubleKeyFrame KeyTime="00:00:00.25" Value="0"/>
  </DoubleAnimationUsingKeyFrames>
 </Storyboard>

Run the app and click the button. Notice how the same old behaviour is there, this is because we haven’t hooked up the StoryBoard animations to anything yet! Change the <ControlTemplate.Triggers> so that it reads as follows

 <ControlTemplate.Triggers>
  <Trigger Property="IsPressed" Value="True">
   <Trigger.EnterActions>
    <BeginStoryboard Storyboard="{StaticResource ButtonDownTimeLine}"/>
   </Trigger.EnterActions>
   <Trigger.ExitActions>
    <BeginStoryboard Storyboard="{StaticResource ButtonUpTimeLine}"/>
   </Trigger.ExitActions>
  </Trigger>
 </ControlTemplate.Triggers>

This will now start the relevant animation whenever the trigger occurs. However, we have lost some behaviour. Previously the contents would offset when the button was down. This was achieved by changing the margin of the "Contents" from 3,3,3,3 to 4,4,2,2 - to reintroduce this behaviour we need to add another animation within the two StoryBoards. As this property is of type Thickness we will need a <ThicknessAnimationUsingKeyFrames> node.

 <ControlTemplate.Resources>
  <Storyboard x:Key="ButtonDownTimeLine">
   <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity">
    <SplineDoubleKeyFrame KeyTime="00:00:00.05" Value="1"/>
   </DoubleAnimationUsingKeyFrames>
   <ThicknessAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Contents" Storyboard.TargetProperty="Margin">
    <SplineThicknessKeyFrame KeyTime="00:00:00.025" Value="4,4,2,2"/>
   </ThicknessAnimationUsingKeyFrames>
  </Storyboard>
  <Storyboard x:Key="ButtonUpTimeLine">
   <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity">
    <SplineDoubleKeyFrame KeyTime="00:00:00.25" Value="0"/>
   </DoubleAnimationUsingKeyFrames>
   <ThicknessAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Contents" Storyboard.TargetProperty="Margin">
    <SplineThicknessKeyFrame KeyTime="00:00:00.25" Value="3,3,3,3"/>
   </ThicknessAnimationUsingKeyFrames>
  </Storyboard>
 </ControlTemplate.Resources>

Now the margin will also animate.


Here is the XAML in its entirety, just so I can copy/paste it myself at a later date :-)

    <Style TargetType="{x:Type Button}">
      <Setter Property="Foreground" Value="White"/>
    </Style>
    <ControlTemplate x:Key="GlassButton" TargetType="{x:Type Button}">
      <Border BorderThickness="1,1,1,1" CornerRadius="4,4,4,4" BorderBrush="Black" Background="Black">
        <Grid>
          <Border x:Name="BorderUp" BorderThickness="2,2,2,2" CornerRadius="4,4,4,4">
            <Border.BorderBrush>
              <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1" >
                <GradientStop Color="White" Offset="0"/>
                <GradientStop Color="#222222" Offset="1"/>
              </LinearGradientBrush>
            </Border.BorderBrush>
            <Border.Background>
              <LinearGradientBrush StartPoint=".5,0" EndPoint=".5,1">
                <GradientStop Color="#aaaaff" Offset="0"/>
                <GradientStop Color="#444466" Offset="0.6"/>
                <GradientStop Color="#444444" Offset="1"/>
              </LinearGradientBrush>
            </Border.Background>
          </Border>
          <Border x:Name="BorderDown" BorderThickness="2,2,2,2" CornerRadius="4,4,4,4" Opacity="0">
            <Border.BorderBrush>
              <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1" >
                <GradientStop Color="White" Offset="1"/>
                <GradientStop Color="#222222" Offset="0"/>
              </LinearGradientBrush>
            </Border.BorderBrush>
            <Border.Background>
              <LinearGradientBrush StartPoint=".5,0" EndPoint=".5,1">
                <GradientStop Color="#aaaaff" Offset="0"/>
                <GradientStop Color="#444466" Offset="0.6"/>
                <GradientStop Color="#444444" Offset="1"/>
              </LinearGradientBrush>
            </Border.Background>
          </Border>
          <ContentPresenter x:Name="Contents" HorizontalAlignment="Center" VerticalAlignment="Center" Width="Auto" Margin="3,3,3,3"/>
        </Grid>
      </Border>
      <ControlTemplate.Resources>
        <Storyboard x:Key="MouseDownTimeLine">
          <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity">
            <SplineDoubleKeyFrame KeyTime="00:00:00.05" Value="1"/>
          </DoubleAnimationUsingKeyFrames>
          <ThicknessAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Contents" Storyboard.TargetProperty="Margin">
            <SplineThicknessKeyFrame KeyTime="00:00:00.025" Value="4,4,2,2"/>
          </ThicknessAnimationUsingKeyFrames>
        </Storyboard>
        <Storyboard x:Key="MouseUpTimeLine">
          <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity">
            <SplineDoubleKeyFrame KeyTime="00:00:00.25" Value="0"/>
          </DoubleAnimationUsingKeyFrames>
          <ThicknessAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Contents" Storyboard.TargetProperty="Margin">
            <SplineThicknessKeyFrame KeyTime="00:00:00.25" Value="3,3,3,3"/>
          </ThicknessAnimationUsingKeyFrames>
        </Storyboard>
      </ControlTemplate.Resources>
      <ControlTemplate.Triggers>
        <Trigger Property="IsPressed" Value="True">
          <Trigger.EnterActions>
            <BeginStoryboard Storyboard="{StaticResource MouseDownTimeLine}"/>
          </Trigger.EnterActions>
          <Trigger.ExitActions>
            <BeginStoryboard Storyboard="{StaticResource MouseUpTimeLine}"/>
          </Trigger.ExitActions>
        </Trigger>
      </ControlTemplate.Triggers>
    </ControlTemplate>

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!