Monday 15 September 2008

Dynamic Data: Part 3-FileUpload FieldTemplates

  1. Part 1 - FileImage_Edit FieldTemplate.
  2. Part 2 - FileImage_Edit FieldTemplate.
  3. Part 3 - FileUpload FiledTemplate.

FileUpload and FileUpload_Edit FiledTemplates

I thought this would complement the DBImage and FileImage FieldTemplates and so I thought what would you want to be able to do:

  • Upload a file to a specified folder.
  • Download the said file once uploaded.
  • Display an image for the file.
  • Control the download capability via attributes and user Roles.
  • Handle errors such as wrong file type or when file is missing from upload folder.

The FileUpload Attributes

In this example I’m creating on attribute to hold all the parameters to do with FileUpload.

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class FileUploadAttribute : Attribute
{
    /// <summary>
    /// where to save files
    /// </summary>
    public String FileUrl { get; set; }

    /// <summary>
    /// File tyoe to allow upload
    /// </summary>
    public String[] FileTypes { get; set; }

    /// <summary>
    /// image type to use for displaying file icon
    /// </summary>
    public String DisplayImageType { get; set; }

    /// <summary>
    /// where to find file type icons
    /// </summary>
    public String DisplayImageUrl { get; set; }

    /// <summary>
    /// If present user must be a member of one
    /// of the roles to be able to download file
    /// </summary>
    public String[] HyperlinkRoles { get; set; }

    /// <summary>
    /// Used to Disable Hyperlink (Enabled by default)
    /// </summary>
    public Boolean DisableHyperlink { get; set; }

    /// <summary>
    /// helper method to check for roles in this attribute
    /// the comparison is case insensitive
    /// </summary>
    /// <param name="role"></param>
    /// <returns></returns>
    public bool HasRole(String[] roles)
    {
        if (HyperlinkRoles.Count() > 0)
        {
            var hasRole = from hr in HyperlinkRoles.AsEnumerable()
                          join r in roles.AsEnumerable()
                          on hr.ToLower() equals r.ToLower()
                          select true;

            return hasRole.Count() > 0;
        }
        return false;
    }

}
Listing 1 – FileUploadAttribute

You will notice in the Listing 1 that all the properties are using c# 3.0’s new Automatic Properties feature less typing; just type prop and hit tab twice and there is you property ready to be filled in.

The second thing you will see is the HasRoles method on this attribute, which takes an array of roles and checks to see if HyperlinkRoles property has any matches. It does this by joining the two arrays together in a Linq to Object query and then selects true for each match in the join. I’m sure this is more readable that the traditional nested foreach loops, it’s certainly neater :D.

The FileUpload FieldTemplate

This FiledTemplate will show the filename and associated icon.

FileUpload with icon

Figure 1- File and associated Icon

<%@ Control
    Language="C#"
    AutoEventWireup="true"
    CodeFile="FileUpload.ascx.cs"
    Inherits="FileImage" %>

<asp:Image ID="Image1" runat="server" />&nbsp;
<asp:Label ID="Label1" runat="server" Text="<%# FieldValueString %>"></asp:Label>
<asp:HyperLink ID="HyperLink1" runat="server"></asp:HyperLink>&nbsp;
<asp:CustomValidator
    ID="CustomValidator1"
    runat="server"
    ErrorMessage="">
</asp:CustomValidator>

Listing 2 – FileUpload.ascx file

As you can see from Listing 1 there are Image, Label and Hyperlink controls on the page. The Label and Hyperlink are mutually exclusive if the conditions are right then a Hyperlink will show so that the file can be downloaded else just a Label will show with the filename.

using System;
using System.IO;
using System.Linq;
using System.Web.DynamicData;
using System.Web.Security;
using System.Web.UI;
using Microsoft.Web.DynamicData;

public partial class FileImage : FieldTemplateUserControl
{

    public override Control DataControl
    {
        get
        {
            return Label1;
        }
    }

    protected override void OnDataBinding(EventArgs e)
    {
        base.OnDataBinding(e);

        //check if field has a value
        if (FieldValue == null)
            return;

        // get the file extension
        String extension = FieldValueString.Substring(
            FieldValueString.LastIndexOf(".") + 1,
            FieldValueString.Length - (FieldValueString.LastIndexOf(".") + 1));

        // get attributes
        var fileUploadAttributes = MetadataAttributes.OfType<FileUploadAttribute>().FirstOrDefault();
        String fileUrl = fileUploadAttributes.FileUrl;
        String displayImageUrl = fileUploadAttributes.DisplayImageUrl;
        String displayImageType = fileUploadAttributes.DisplayImageType;


        // check the file exists else throw validation error
        String filePath;
        if (fileUploadAttributes != null)
            filePath = String.Format(fileUrl, FieldValueString);
        else
            // if attribute not set use default
            filePath = String.Format("~/files/{0}", FieldValueString);

        // show the relavent control depending on metadata
        if (fileUploadAttributes.HyperlinkRoles.Length > 0)
        {
            // if there are roles then check: 
            // if user is in one of the roles supplied
            // or if the hyperlinks are disabled 
            // or if the file does not exist
            // then hide the link
            if (!fileUploadAttributes.HasRole(Roles.GetRolesForUser())  fileUploadAttributes.DisableHyperlink  !File.Exists(Server.MapPath(filePath)))
            {
                Label1.Text = FieldValueString;
                HyperLink1.Visible = false;
            }
            else
            {
                Label1.Visible = false;
                HyperLink1.Text = FieldValueString;
                HyperLink1.NavigateUrl = filePath;
            }
        }
        else
        {
            // if either hyperlinks are disabled or the
            // file does not exist then hide the link
            if (fileUploadAttributes.DisableHyperlink  !File.Exists(Server.MapPath(filePath)))
            {
                Label1.Text = FieldValueString;
                HyperLink1.Visible = false;
            }
            else
            {
                Label1.Visible = false;
                HyperLink1.Text = FieldValueString;
                HyperLink1.NavigateUrl = filePath;
            }
        }

        // check file exists on file system
        if (!File.Exists(Server.MapPath(filePath)))
        {
            CustomValidator1.ErrorMessage = String.Format("{0} does not exist", FieldValueString);
            CustomValidator1.IsValid = false;
        }

        // show the icon
        if (!String.IsNullOrEmpty(extension))
        {
            // set the file type image
            if (!String.IsNullOrEmpty(displayImageUrl))
            {
                Image1.ImageUrl = String.Format(displayImageUrl, extension + "." + displayImageType);
            }
            else
            {
                // if attribute not set the use default
                Image1.ImageUrl = String.Format("~/images/{0}", extension + "." + displayImageType);
            }

            Image1.AlternateText = extension + " file";

            // if you apply dimentions from DD Futures
            var imageFormat = MetadataAttributes.OfType<ImageFormatAttribute>().FirstOrDefault();
            if (imageFormat != null)
            {
                // if either of the dims is 0 don't set it
                // this will mean that the aspect will remain locked
                if (imageFormat.DisplayWidth != 0)
                    Image1.Width = imageFormat.DisplayWidth;
                if (imageFormat.DisplayHeight != 0)
                    Image1.Height = imageFormat.DisplayHeight;
            }
        }
        else
        {
            // if file has no extension then hide image
            Image1.Visible = false;
        }
    }
}

Listing 3 – FileUpload.ascx.cs file

In Listing 3 you can see that everything goes on in the OnDataBinding event handler.

The FileUpload_Edit FieldTemplate

<%@ Control
    Language="C#"
    AutoEventWireup="true"
    CodeFile="FileUpload_Edit.ascx.cs"
    Inherits="FileImage_Edit" %>
   
<asp:PlaceHolder ID="PlaceHolder1" runat="server" Visible="false">
    <asp:Image ID="Image1" runat="server" />&nbsp;
    <asp:Label ID="Label1" runat="server" Text="<%# FieldValueString %>"></asp:Label>
    <asp:HyperLink ID="HyperLink1" runat="server"></asp:HyperLink>&nbsp;
</asp:PlaceHolder>
<asp:FileUpload ID="FileUpload1" runat="server" />&nbsp;
<asp:CustomValidator
    ID="CustomValidator1"
    runat="server"
    ErrorMessage="">
</asp:CustomValidator>

Listing 4 – FileUpload_Edit.ascx file

In Listing 4 the PlaceHolder control is used to hide the Image, Label and Hyperlink when in insert mode or when there is no value to be shown.

using System;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Web.DynamicData;
using System.Web.Security;
using System.Web.UI;
using Microsoft.Web.DynamicData;

public partial class FileImage_Edit : FieldTemplateUserControl
{
    public override Control DataControl
    {
        get
        {
            return Label1;
        }
    }

    protected override void OnDataBinding(EventArgs e)
    {
        base.OnDataBinding(e);

        //check if field has a value
        if (FieldValue == null)
            return;

        // when there is already a value in the FieldValue
        // then show the icon and label/hyperlink
        PlaceHolder1.Visible = true;

        // get the file extension
        String extension = FieldValueString.Substring(
            FieldValueString.LastIndexOf(".") + 1,
            FieldValueString.Length - (FieldValueString.LastIndexOf(".") + 1));

        // get attributes
        var fileUploadAttributes = MetadataAttributes.OfType<FileUploadAttribute>().FirstOrDefault();
        String fileUrl = fileUploadAttributes.FileUrl;
        String displayImageUrl = fileUploadAttributes.DisplayImageUrl;
        String displayImageType = fileUploadAttributes.DisplayImageType;
        String filePath;

        // check the file exists else throw validation error
        if (fileUploadAttributes != null)
            filePath = String.Format(fileUrl, FieldValueString);
        else
            // if attribute not set use default
            filePath = String.Format("~/files/{0}", FieldValueString);

        // show the relavent control depending on metadata
        if (fileUploadAttributes.HyperlinkRoles.Length > 0)
        {
            // if there are roles then check: 
            // if user is in one of the roles supplied
            // or if the hyperlinks are disabled 
            // or if the file does not exist
            // then hide the link
            if (!fileUploadAttributes.HasRole(Roles.GetRolesForUser())  fileUploadAttributes.DisableHyperlink  !File.Exists(Server.MapPath(filePath)))
            {
                Label1.Text = FieldValueString;
                HyperLink1.Visible = false;
            }
            else
            {
                Label1.Visible = false;
                HyperLink1.Text = FieldValueString;
                HyperLink1.NavigateUrl = filePath;
            }
        }
        else
        {
            // if either hyperlinks are disabled or the
            // file does not exist then hide the link
            if (fileUploadAttributes.DisableHyperlink  !File.Exists(Server.MapPath(filePath)))
            {
                Label1.Text = FieldValueString;
                HyperLink1.Visible = false;
            }
            else
            {
                Label1.Visible = false;
                HyperLink1.Text = FieldValueString;
                HyperLink1.NavigateUrl = filePath;
            }
        }

        // check file exists on file system
        if (!File.Exists(Server.MapPath(filePath)))
        {
            CustomValidator1.ErrorMessage = String.Format("{0} does not exist", FieldValueString);
            CustomValidator1.IsValid = false;
        }

        // show the icon
        if (!String.IsNullOrEmpty(extension))
        {
            // set the file type image
            if (!String.IsNullOrEmpty(displayImageUrl))
            {
                Image1.ImageUrl = String.Format(displayImageUrl, extension + "." + displayImageType);
            }
            else
            {
                // if attribute not set the use default
                Image1.ImageUrl = String.Format("~/images/{0}", extension + "." + displayImageType);
            }

            Image1.AlternateText = extension + " file";

            // if you apply dimentions from DD Futures
            var imageFormat = MetadataAttributes.OfType<ImageFormatAttribute>().FirstOrDefault();
            if (imageFormat != null)
            {
                // if either of the dims is 0 don't set it
                // this will mean that the aspect will remain locked
                if (imageFormat.DisplayWidth != 0)
                    Image1.Width = imageFormat.DisplayWidth;
                if (imageFormat.DisplayHeight != 0)
                    Image1.Height = imageFormat.DisplayHeight;
            }
        }
        else
        {
            // if file has no extension then hide image
            Image1.Visible = false;
        }
    }

    protected override void ExtractValues(IOrderedDictionary dictionary)
    {
        // get attributes
        var fileUploadAttributes = MetadataAttributes.OfType<FileUploadAttribute>().FirstOrDefault();

        String fileUrl;
        String[] extensions;
        if (fileUploadAttributes != null)
        {
            fileUrl = fileUploadAttributes.FileUrl;
            extensions = fileUploadAttributes.FileTypes;

            if (FileUpload1.HasFile)
            {
                // get the files folder
                String filesDir = fileUrl.Substring(0, fileUrl.LastIndexOf("/") + 1);

                // resolve full path c:\... etc
                String path = Server.MapPath(filesDir);

                // get files extension without the dot
                String fileExtension = FileUpload1.FileName.Substring(
                    FileUpload1.FileName.LastIndexOf(".") + 1).ToLower();

                // check file has an allowed file extension
                if (extensions.Contains(fileExtension))
                {
                    // try to upload the file showing error if it fails
                    try
                    {
                        FileUpload1.PostedFile.SaveAs(path + "\\" + FileUpload1.FileName);
                        Image1.ImageUrl = String.Format(fileUploadAttributes.DisplayImageUrl, fileExtension + ".png");
                        Image1.AlternateText = fileExtension + " file";
                        dictionary[Column.Name] = FileUpload1.FileName;
                    }
                    catch (Exception ex)
                    {
                        // display error
                        CustomValidator1.IsValid = false;
                        CustomValidator1.ErrorMessage = ex.Message;
                    }
                }
                else
                {
                    CustomValidator1.IsValid = false;
                    CustomValidator1.ErrorMessage = String.Format("{0} is not a valid file to upload", FieldValueString);
                }
            }
        }
    }
}
Listing 5 - FileUpload_Edit.ascx.cs file

In Listing 5 the OnDataBinding event handler is pretty much the same as the FileUpload.ascs.cs file. Here its the ExtractValues method that does the work of uploading and displaying errors, i.e. if the file type of the file to be uploaded does not match a file type specified in the metadata or there is an error during the upload.

Helper Class FileUploadHelper

public static class FileUploadHelper
{
    /// <summary>
    /// If the given table contains a column that has a UI Hint with the value "DbImage", finds the ScriptManager
    /// for the current page and disables partial rendering
    /// </summary>
    /// <param name="page"></param>
    /// <param name="table"></param>
    public static void DisablePartialRenderingForUpload(Page page, MetaTable table)
    {
        foreach (var column in table.Columns)
        {
            // TODO this depends on the name of the field template, need to fix
            if (String.Equals(
                column.UIHint, "DBImage", StringComparison.OrdinalIgnoreCase)
                String.Equals(column.UIHint, "FileImage", StringComparison.OrdinalIgnoreCase)
                String.Equals(column.UIHint, "FileUpload", StringComparison.OrdinalIgnoreCase))
            {
                var sm = ScriptManager.GetCurrent(page);
                if (sm != null)
                {
                    sm.EnablePartialRendering = false;
                }
                break;
            }
        }
    }
}

Listing 6 - FileUploadHelper

This is just a modified version of the Dynamic Data Futures DisablePartialRenderingForUpload method the only difference is that I’ve added support for both my file upload capable FieldTemplates FileImage and FileUpload.

Finally Some Sample Metadata

[MetadataType(typeof(FileImageTestMD))]
public partial class FileImageTest : INotifyPropertyChanging, INotifyPropertyChanged
{
    public class FileImageTestMD
    {
        public object Id { get; set; }
        public object Description { get; set; }
        [UIHint("FileUpload")]
        [FileUpload(
            FileUrl = "~/files/{0}",
            FileTypes = new String[] { "pdf", "xls", "doc", "xps" },
            DisplayImageType = "png",
            DisableHyperlink = false,
            HyperlinkRoles=new String[] { "Admin", "Accounts" },
            DisplayImageUrl = "~/images/{0}")]
        [ImageFormat(22, 0)]
        public object filePath { get; set; }
    }
}

Listing 7 – sample metadata

The FileUpload Project

Note: Please note that the ASPNETDB.MDF supplied in this website is SQL 2008 Express and will not work with SQL 2005 and earlier, you will need to set your own up. Or you can just strip out the login capability from the site.master and web.config.

Enjoy smile_teeth

31 comments:

Unknown said...

Hi,
I was wondering how this would change if you also used your security / attributes process? I attempted to combine both techniques, but the partial rendering on the image does not seem to work in combination with your security technique.

Stephen J. Naughton said...

Hi Fred, I'm not sure why there there would be a problem combining these two, but my first step to checking this from what you say would be to disable PartialRendering on the Site.Master page. and see what happens there. :D

Steve

Anonymous said...

Couldn't get this working, so did some research and it turns out that the FileUpload control doesn't work inside of an UpdatePanel of the AJAX framework. You have to add a PostBackTrigger according to this reference:

    http://www.codeproject.com/KB/ajax/simpleajaxupload.aspx


Just wanted to mention, so that others aren't scratching their heads like I was. In addition, I encountered a null reference error in FileUpload_Edit.ascx.cs, line 52 on this conditional:

    if (fileUploadAttributes.HyperlinkRoles.Length > 0)

The equivalent line in FileUpload.ascx.cs checks this instead:

    if (fileUploadAttributes.HyperLink
roles != null)

Nesim T. said...

Hi,

Firstly, thank you very much for the perfect article.

I have a problem. We use different application for administration and web ui. I mean there are two virtual directory of my website.

/Admin
/NormalWebSiteFolder

But they are different virtual directories. So admin will upload images to Admin/files but i don't want to user use admin/files for preview of the images.

is it possible to upload a different path of the web site from admin application?

How can i upload files to website "files" folder from different application (different virtual directory on the same iis) to web site.

Thanks in advance.

Stephen J. Naughton said...

If you want a folder in the WebSite folder to have images uploaded by the Admin app the all you need to do is create a virtual folder in the Admin app the point to the folder in the website

Steve :D

Nesim T. said...

Any way i can solve it no problem ...
There is an another problem. FileUpload1.Hasfile is always false. :) I select file and then insert item bu when i am debugging i see it hasn't got a file. :( Is that know issue?

Stephen J. Naughton said...

This is a known issue with Update pannel try setting EnablePartialRendering="false" in the site.master see the DisablePartialRenderingForUpload method which is used in the page to disable PartialRender.

Steve :D

Nesim T. said...

You are perfect. ;)
In Edit & Insert.aspx i called that methods and solved ;)

Thanks a lot steve

Stephen J. Naughton said...

Your welcome, I don't think I made it clear enough in the article.

Steve :D

Ömer Faruk said...

its don't runs on my project. HasFile property of FileUpload component is already returns is false.

how to correct this?
thanks

Stephen J. Naughton said...

That sort of effect may occur in you have not implemented the FileUploadHelper class to disable partial updates on the page. To test to see if this is your issue go to you site.master page and find the ScriptManager tag and change the EnablePartialRendering="true" to EnablePartialRendering="false"

Hope this helps.

Steve :D

Haider said...

I am not sure if it is due to the changes in the latest DynamicData preview, but the validation logic with CustomValidator seems ineffective in ExtractValues() Method. It looks like the framework is past checking IsValid on all validation controls and setting CustomValidator1.IsValid = false is not doing anything. More over, throwing a validationException in this method does not work either, as the DynamicValidator seems to kick in only if a ValidationException is thrown during setting values on the Entities (OnPropertyChanging Events, for example). I fixed it by trapping the CustomValidator_ServerValidate Event and putting the validation logic (in this case, check for valid file extension) in the event handler.

Stephen J. Naughton said...

Thanks Haider, it's a long while ago that I did this and I'm pretty sure that it all worked back then.

Steve

P.S. I'll be comming back to this when VS2010 Beta 2 comes out and redoing all my custom FieldTemplates.

Feng said...

Hi Steve, when i upload a file that is not in the extension list, this line give me an error of : Databinding methods such as Eval(), xpath() and Bind() can only be used in the context of a databound control

if(extensions.contails(fileExtension))
{....}
else
{
CustomValidator1.IsValid = false;
CustomValidator1.ErrorMessage = String.Format("{0} is not a valid file to upload", FieldValueString); // Error throws here
}

any idea?thanks in advance

Stephen J. Naughton said...

It sounds like you have partial render turned on to test this turn it off in ther master page <asp:ScriptManager ID="ScriptManager1" runat="server" EnablePartialRendering="true" /> set EnablePartialRendering to false. If this fixes the issue you missed out a section of the article "Helper Class FileUploadHelper" you will need to look at previouse article in the series to see how to apply it in the idividual pages.

Steve :D

Feng said...

Thanks Steve,still not solved the problem,I had already turn it off in the master page : EnablePartialRendering="false"(otherwise the FileUpload1.HasFile always return false)

and set DisablePartialRenderingForUpload in my TabbedEditWithSubGrids class:
FileUploadHelper.DisablePartialRenderingForUpload(this, table);
FormView1.ItemTemplate = new DetailsSubGridsTemplate(table, Page);

I downloaded the solution in this article, i also got the error as well. I used System.Web.DynamicData.dll v3.5.0.0, thanks again

Stephen J. Naughton said...

Hi Feng, I'll spin the project up tomorrow and get back top you.

Steve :D

Feng said...

Hi, Steve,i redownload the project in the article make sure i got the right code, the issue is still exists..the only thing i change is in web.config file and remove the authorization so i don't need to login

Stephen J. Naughton said...

Hi Feng, I've just tested it on my PC and all seems to work fine insert works and the files are uploaded.

Steve :(

Feng said...

Hi, Steve i got the upload/file insert working fine. but when i try to insert a file with a funny extention, like abc.bak or abc.config, it will throw the exception :)

Stephen J. Naughton said...

Hi Feng, you are correct, I th8ink there are some fundemental errors in this project, I will rework based on the FileImage in the previouse article as you cannot do validation in the Extract values event, it's all too late by then I think there needs to be an upload button to do the validation in.

Steve :(

Feng said...

Right, thanks Steve,i will implement that as well :)

Stephen J. Naughton said...

Sorry About that Feng, but we all make mistakes from time to time :(

Steve

Anonymous said...

File Upload has a problem..It can upload a file,But File path not visible on that textbox.

Stephen J. Naughton said...

Yes there are a number of issues with it, I'm doing a paying project and will re-do the article and then make this one point to the new one in a week or so :(.

I'm going to base the new one on an earlyer article for uploading images as I seem to have introduced several serious bugs here.

Steve :D

Anonymous said...

Hi Steve:

I can't download the source code. Can you send me this .zip project to lrscott83@gmail.com ?

Thanks ...

Anonymous said...

Steve, I have downloaded and expanded the zip file but I can't even open the project. There is no project or solution file. can you please send me the e-mail at

samirbarot@rogers.com

I hope your sample project is working project where I can see or test something before I implement.


Samir Barot

Msbh said...

Hey I am bit new to DD i got ur project run it was nice i want to implment this in my project but donot know where to call Fileimage in project. I have added many Tables in my project and want to implement this on one entity of any one table. having lot problem some one can refer me book or help me out in this
Thanks
MSBH

Stephen J. Naughton said...

it will only work in a Dynam,ic Data project.

Steve

ashuthinks said...

Hi Steve

This is Ashish from India i'm a software engineer working on dynamic file upload control

i have gone through your post

but i want to know more if i need a file upload control like asp.net and upload button also then is it post correct for that as i'm not sure there is any file upload control in this

please help me

Stephen J. Naughton said...

Hi Ashish, just sent you the field template in a zio file

Steve