ASP.NET MVC Lesson 9. Configuration and File Upload

  • Tutorial
The purpose of the lesson. Learn to use the Web.config configuration file. Application section, creating your own ConfigSection and IConfig. Learn to upload files, use file-uploader to download a file, and then process the file.

In this tutorial, we will look at working with the Web.config configuration file. This is an xml file and it stores the program settings.

Let's consider in more detail what this file consists of:
  • configSection . This section is responsible for which classes will process further declared sections. Consists of the attribute name - this is a tag, then the declared section, and type - which class it belongs to.
  • connectionStrings . This section is responsible for working with strings for initializing database connections.
  • appSettings . Section of parameters of type key / value.
  • system.web, system.webServer . Parameter sections for a web application.
  • runtime. Runtime tuning section. Defining dependencies between dlls.
  • The remaining sections. Other sections with parameters declared in configSection.




IConfig (and implementation).

Like Repository, we will create a configurator as a service. Create IConfig and Config implementation in the Global folder (/Global/Config/IConfig.cs):
public interface IConfig
    {
        string Lang { get; }
    }

AND
public class Config : IConfig
    {
        public string Lang
        {
            get 
            {
                return "ru";
            }
        }
    }

Add a line to RegisterServices (/App_Start/NinjectWebCommon.cs):
 kernel.Bind().To().InSingletonScope();

And output to BaseController:
[Inject]
public IConfig Config { get; set; }


Now, in the controller initialization, we will reassign CultureInfo in the stream (/Controllers/BaseController.cs):
protected override void Initialize(System.Web.Routing.RequestContext requestContext)
        {
            try
            {
                var cultureInfo = new CultureInfo(Config.Lang);
                Thread.CurrentThread.CurrentCulture = cultureInfo;
                Thread.CurrentThread.CurrentUICulture = cultureInfo;
            }
            catch (Exception ex)
            {
                logger.Error("Culture not found", ex);
            }
            base.Initialize(requestContext);
        }


And add the date output to Index.cshtml (/Areas/Default/Views/Home/Index.cshtml):
    @DateTime.Now.ToString("D")


We


get the conclusion: And we really associate this with Web.Config. Add the line in Web.config in appSettings:


In Config.cs (/Global/Config/Config.cs):
public string Lang
        {
            get 
            {
return ConfigurationManager.AppSettings["Culture"] as string;         
     }
        }

We start - the result is the same, now change the value in Web.config to fr:

We get the date:
mardi 5 mars 2013


Excellent! You can try with several more languages. The list of abbreviations is here http://msdn.microsoft.com/en-us/goglobal/bb896001.aspx

Creating your own ConfigSection types

In this part, we will look at creating your own ConfigSection. In this chapter, we implement file upload and preview creation. We need the following data: firstly, the dependence of mime-type on the extension, and the file icon (for download, for example):
  • expansion
  • mime-type
  • large icon
  • small icon


and secondly, data for creating a preview:
  • preview title (e.g. UserAvatarSize)
  • width
  • height


Both types are done the same way, so I will only write the creation of one of them. Let it be IconSize, to create a preview. The first thing to do is to create a class inherited by ConfigurationElement (/Global/Config/IconSize.cs):
public class IconSize : ConfigurationElement
    {
        [ConfigurationProperty("name", IsRequired = true, IsKey = true)]
        public string Name
        {
            get
            {
                return this["name"] as string;
            }
        }
        [ConfigurationProperty("width", IsRequired = false, DefaultValue = "48")]
        public int Width
        {
            get
            {
                return (int)this["width"];
            }
        }
        [ConfigurationProperty("height", IsRequired = false, DefaultValue = "48")]
        public int Height
        {
            get
            {
                return (int)this["height"];
            }
        }
    }


Let's consider in more detail:
  • ConfigurationProperty consists of the name, this is the attribute name in the string
  • IsRequired - mandatory or not
  • IsKey - is the key (as the primary key in the database)
  • DefaultValue - default value


The next step is to create a collection class (since we will have many elements) and a section (/Global/Config/IconSize.cs):
 public class IconSizesConfigSection : ConfigurationSection
    {
        [ConfigurationProperty("iconSizes")]
        public IconSizesCollection IconSizes
        {
            get
            {
                return this["iconSizes"] as IconSizesCollection;
            }
        }
    }
    public class IconSizesCollection : ConfigurationElementCollection
    {
        protected override ConfigurationElement CreateNewElement()
        {
            return new IconSize();
        }
        protected override object GetElementKey(ConfigurationElement element)
        {
            return ((IconSize)element).Name;
        }
}

In Web.config, add:


Now you need to declare a parsing class for this section in configSection:


Note that in the type description you must specify the name dll ( LessonProject) in which it is contained. This is important, but will be covered in unit tests.

Mailsettings

Let's create a single config for settings for working with smtp-mail. We will need:
  • SmtpServer. Server name
  • SmtpPort Port, usually 25th.
  • SmtpUserName. Login.
  • SmtpPassword. Password.
  • SmtpReply. The return address in the Reply-to line.
  • SmtpUser. The username in the From line.
  • EnableSsl. Yes / no, whether to use work on Ssl.


File (/Global/Config/MailSetting.cs):
public class MailSetting : ConfigurationSection
    {
        [ConfigurationProperty("SmtpServer", IsRequired = true)]
        public string SmtpServer
        {
            get
            {
                return this["SmtpServer"] as string;
            }
            set
            {
                this["SmtpServer"] = value;
            }
        }
        [ConfigurationProperty("SmtpPort", IsRequired = false, DefaultValue="25")]
        public int SmtpPort
        {
            get
            {
                return (int)this["SmtpPort"];
            }
            set
            {
                this["SmtpPort"] = value;
            }
        }
        [ConfigurationProperty("SmtpUserName", IsRequired = true)]
        public string SmtpUserName
        {
            get
            {
                return this["SmtpUserName"] as string;
            }
            set
            {
                this["SmtpUserName"] = value;
            }
        }
        [ConfigurationProperty("SmtpPassword", IsRequired = true)]
        public string SmtpPassword
        {
            get
            {
                return this["SmtpPassword"] as string;
            }
            set
            {
                this["SmtpPassword"] = value;
            }
        }
        [ConfigurationProperty("SmtpReply", IsRequired = true)]
        public string SmtpReply
        {
            get
            {
                return this["SmtpReply"] as string;
            }
            set
            {
                this["SmtpReply"] = value;
            }
        }
        [ConfigurationProperty("SmtpUser", IsRequired = true)]
        public string SmtpUser
        {
            get
            {
                return this["SmtpUser"] as string;
            }
            set
            {
                this["SmtpUser"] = value;
            }
        }
        [ConfigurationProperty("EnableSsl", IsRequired = false, DefaultValue="false")]
        public bool EnableSsl
        {
            get
            {
                return (bool)this["EnableSsl"];
            }
            set
            {
                this["EnableSsl"] = value;
            }
        }
    }


Add to Web.config:

AND

Add all this now to IConfig.cs and Config.cs (/Global/Config/IConfig.cs):
public interface IConfig
    {
        string Lang { get; }
        IQueryable IconSizes { get; }
        IQueryable MimeTypes { get; }
        MailSetting MailSetting { get; }
    }


AND
public IQueryable IconSizes
        {
            get 
            {
                IconSizesConfigSection configInfo = (IconSizesConfigSection)ConfigurationManager.GetSection("iconConfig");
                return configInfo.IconSizes.OfType().AsQueryable(); 
            }
        }
        public IQueryable MimeTypes
        {
            get
            {
                MimeTypesConfigSection configInfo = (MimeTypesConfigSection)ConfigurationManager.GetSection("mimeConfig");
                return configInfo.MimeTypes.OfType().AsQueryable();
            }
        }
        public MailSetting MailSetting
        {
            get 
            { 
                return (MailSetting)ConfigurationManager.GetSection("mailConfig");
            }
        }


We will also add MailTemplates - templates that we will need to send out email when registering, or when reminding a password.

Easy file upload


Now let's look at a standard example of uploading a file to the server, and we will never use this method again. SimpleFileView class for interaction (/Models/Info/SimpleFileView.cs):
public class SimpleFileView
    {
        public HttpPostedFileBase UploadedFile { get; set; }
    }

Pay attention to the class name for receiving files. So, create the SimpleFileController controller (/Areas/Default/Controllers/SimpleFileController.cs):
public class SimpleFileController : DefaultController
    {
        [HttpGet]
        public ActionResult Index()
        {
            return View(new SimpleFileView());
        }
        [HttpPost]
        public ActionResult Index(SimpleFileView simpleFileView)
        {
            return View(simpleFileView);
        }
    }


And add View:
@model LessonProject.Models.Info.SimpleFileView
@{
    ViewBag.Title = "Index";
    Layout = "~/Areas/Default/Views/Shared/_Layout.cshtml";
}

Index

@using (Html.BeginForm("Index", "SimpleFile", FormMethod.Post, new {enctype = "multipart/form-data", @class = "form-horizontal" })) {
@Html.TextBox("UploadedFile", Model.UploadedFile, new { type = "file", @class = "input-xlarge" }) @Html.ValidationMessage("UploadedFile")
}


Pay attention to enctype in the attributes of the form and to type in the attributes of the TextBox (in fact, the type is still password, checkbox, radio, but there are corresponding methods for them in the @ Html class). Enctype must be set to “multipart / form-data” in order to be able to load a large amount of information.

Download and check. Our file is uploaded safely, you just need to save the InputStream to a certain file. But let's leave it for now and consider the shortcomings.

The first drawback is that in all browsers the file selection form looks different:



Of course, after all, the designer imagines that the files are downloaded as in Safari, and the customer checks in Chrome and IE, and starts asking the developers: “What kind of initiative? "
The second drawback is that if the form has not passed validation, then these fields must be selected again. Those. there is such a form:
  • Name
  • Surname
  • Email
  • Date of Birth
  • The photo
  • Photo of the first spread of the passport
  • Photo of the second spread of the passport
  • Passport photo with registration
  • Password
  • The password again
  • Captcha


And suddenly you typed the password incorrectly, or you entered the captcha incorrectly, or the photo of the second turn of your passport is too large, or you forgot to overtake from raw format to jpeg.

As a result, photos, registration and captcha must be re-entered. Naturally, this is not user friendly at all, and annoys the customer (besides, the designer painted beautifully, but looks wretched).

Upload file (s) using Ajax

Determine how the file upload should behave:
  • The user clicks on “download”.
  • The file selection form opens
  • User selects file
  • The file is loading, or an error is issued stating that something is wrong
  • Even if the form does not pass validation, the file remains loaded and does not need to be downloaded again.


This is called ajax loading and we use fineuploader for it ( http://fineuploader.com/ ). The library is paid, but we download and collect the sources (we have bundle!). Download the sources at the link: https://github.com/valums/file-uploader . Move the js files to the / Scripts / fine-uploader folder. We move the css files to / Content and the images to / Content / images. We rewrite the url correctly in fineuploader.css for images:
.qq-upload-spinner {
    display: inline-block;
    background: url("images/loading.gif");
    width: 15px;
    height: 15px;
    vertical-align: text-bottom;
}
.qq-drop-processing {
    display: none;
}
.qq-drop-processing-spinner {
    display: inline-block;
    background: url("images/processing.gif");
    width: 24px;
    height: 24px;
    vertical-align: text-bottom;
}


We initialize the files in BundleConfig.cs (/App_Start/BundleConfig.cs):

bundles.Add(new ScriptBundle("~/bundles/fineuploader")
                    .Include("~/Scripts/fine-uploader/header.js")
                    .Include("~/Scripts/fine-uploader/util.js")
                    .Include("~/Scripts/fine-uploader/button.js")
                    .Include("~/Scripts/fine-uploader/ajax.requester.js")
                    .Include("~/Scripts/fine-uploader/deletefile.ajax.requester.js")
                    .Include("~/Scripts/fine-uploader/handler.base.js")
                    .Include("~/Scripts/fine-uploader/window.receive.message.js")
                    .Include("~/Scripts/fine-uploader/handler.form.js")
                    .Include("~/Scripts/fine-uploader/handler.xhr.js")
                    .Include("~/Scripts/fine-uploader/uploader.basic.js")
                    .Include("~/Scripts/fine-uploader/dnd.js")
                    .Include("~/Scripts/fine-uploader/uploader.js")
                    .Include("~/Scripts/fine-uploader/jquery-plugin.js")
                    );
bundles.Add(new StyleBundle("~/Content/css/fineuploader")
                 .Include("~/Content/fineuploader.css"));


Create the controller FileController.cs (/Areas/Default/Controllers/FileController.cs):
public class FileController : DefaultController
    {
        [HttpGet]
        public ActionResult Index()
        {
            return View();
        }
        public ActionResult Upload(HttpPostedFileWrapper qqfile)
        {
            return Json(new { result = "ok", success = true});
        }
    }

The action-upload method accepts the qqfile string value, I will discuss below why. Now create a View for Index. For this:
  • Create a button, when clicked, we download the file.
  • File is uploaded and preview created.
  • File and preview saved to file system
  • The method returns the link where the file and preview were uploaded via the Json response
  • If the files could not be downloaded, an appropriate error is issued
  • We process the json result and notify that the file and preview are loaded
  • Form verification and writing to the database are not needed.


View for Index:
@{
    ViewBag.Title = "Index";
    Layout = "~/Areas/Default/Views/Shared/_Layout.cshtml";
}
@section styles {
    @Styles.Render("~/Content/css/fineuploader")
}
@section scripts {
    @Scripts.Render("~/bundles/fineuploader")
    @Scripts.Render("~/Scripts/default/file-index.js")
}

Index

Upload


Our button with id = UploadImage. Add file-index.js file for processing (/Scripts/default/file-index.js):
function FileIndex() {
    _this = this;
    this.ajaxFileUpload = "/File/Upload";
    this.init = function () {
        $('#UploadImage').fineUploader({
            request: {
                endpoint: _this.ajaxFileUpload
            },
        }).on('error', function (event, id, name, reason) {
            //do something
        })
      .on('complete', function (event, id, name, responseJSON) {
          alert(responseJSON);
      });
    }
}
var fileIndex = null;
$().ready(function () {
    fileIndex = new FileIndex();
    fileIndex.init();
});


Now process the download:
public ActionResult Upload(HttpPostedFileWrapper qqfile)
        {
            var extension = Path.GetExtension(qqfile.FileName);
            if (!string.IsNullOrWhiteSpace(extension))
            {
                var mimeType = Config.MimeTypes.FirstOrDefault(p => string.Compare(p.Extension, extension, 0) == 0);
                //если изображение
                if (mimeType.Name.Contains("image"))
                {
                    //тут сохраняем в файл
                    var filePath = Path.Combine("/Content/files", qqfile.FileName);
                    qqfile.SaveAs(Server.MapPath(filePath));    
                    return Json(new
                    {
                        success = true,
                        result = "error",
                        data = new
                        {
                            filePath
                        }
                    });
                }
            }
            return Json(new { error = "Нужно загрузить изображение", success = false });
        } 


Add the files folder to Content - this will be the user data folder. Let's analyze the code:
  • We get qqfile (do not change anything here, this parameter is due to fineuploader).
  • From it we get extension.
  • By extension we find mimeType. For .jpg, .gif, .png - we get a mime-type of type "image / ...". Thus, we verify that this file can be downloaded.
  • Next, using the file name, we compose the absolute path to the / Content / files folder (which we created in advance) using Server.MapPath.
  • Next, save the file using SaveAs.
  • Return the file name in json data.filePath.


We check whether everything is loading, and proceed to create a preview.

Preview Creation

Firstly, we cheated a little with mime-type = "image \ ...", because these include bmp and tiff files that are not supported by browsers.
So let's create the PreviewCreator class in the LessonProject.Tools (PreviewCreator.cs) project:
   public static class PreviewCreator
    {
public static bool SupportMimeType(string mimeType)
        {
            switch (mimeType)
            {
                case "image/jpg":
                case "image/jpeg":
                case "image/png":
                case "image/gif":
                    return true;
            }
            return false;
        }
    }


And replace in FileController.cs (/Areas/Default/Controller/FileController.cs):
if (mimeType != null && PreviewCreator.SupportMimeType(mimeType.Name))


PreviewCreator has many functions for creating previews, so I will list the different options for creating an image and analyze one of them in detail. It is worth considering that all previews are created in jpeg format. So what are the options:
  • Color and black and white option. Controlled by the grayscale parameter (default = false)
  • Preview ( CreateAndSavePreview) If the original image is smaller than the dimensions of the preview, then the image is placed in the middle of a white canvas. If in relation to the sizes the initial size has a vertical orientation (a square from the portrait format) - cut out the upper part. If the ratio is horizontally oriented with respect to size, then cut out the middle.
  • Аватар. (CreateAndSaveAvatar) Если исходное изображение меньше, чем размеры превью, то изображение просто сохраняется. Если по отношению к размерам исходный размер имеет вертикальную ориентированность (квадратик из портретного формата) – то уменьшаем, по высоте. Если же отношение горизонтально ориентированно относительно размера, то вырезаем середину.
  • Изображение. (CreateAndSaveImage) Если изображение меньше, чем максимальные размеры, то сохраняем исходное. Если же изображение не вписывается в границы, то уменьшаем, чтобы оно не превышало максимальный размер, и сохраняем.
  • По размеру. (CreateAndSaveFitToSize) Если изображение меньше, чем размеры, то оно будет растянуто до необходимых размеров. С потерей качества, конечно же.
  • Обрезать. (CropAndSaveImage) Кроме стандартных параметров передаются координаты для обрезки изображения.


Let's create a preview ( CreateAndSavePreview), taking the dimensions from the configuration to create the AvatarSize preview (/Areas/Default/Controllers/FileController.cs):
var filePreviewPath = Path.Combine("/Content/files/previews", qqfile.FileName);
                    var previewIconSize = Config.IconSizes.FirstOrDefault(c => c.Name == "AvatarSize");
                    if (previewIconSize != null)
                    {
                        PreviewCreator.CreateAndSavePreview(qqfile.InputStream, new Size(previewIconSize.Width, previewIconSize.Height), Server.MapPath(filePreviewPath));
                    }
return Json(new
                    {
                        success = true,
                        result = "error",
                        data = new
                        {
                            filePath,
                            filePreviewPath
                        }
                    });


We start. Download. Files must load and a preview is created.
Now we will do the processing in file-index.js (/Scripts/default/file-index.js):
$('#UploadImage').fineUploader({
            request: {
                endpoint: _this.ajaxFileUpload
            },
        })
        .on('error', function (event, id, name, reason) {
            //do something
        })
        .on('complete', function (event, id, name, responseJSON) {
          $("#ImagePreview").attr("src", responseJSON.data.filePreviewPath);
        });


now our file is loaded with the preview. The path of a large file can also be transferred separately, and recorded, for example, in a hidden field and saved in the future as a string in the database.
What is wrong with this design is the following two problems:
  • files can be overwritten, but this is solved by the fact that you can only take the extension and assign the file name separately, or add a little salt
  • files can be downloaded and not related to the database. This can be solved by the fact that for each table the files are written to a separate folder, and then do a search and delete unrecorded ones.


Retrieving Files by Link

There is another file upload method. The file hangs freely on the Internet, and we indicate the path to it (for example, when logging in with facebook), and we already save this file using the link.
This is done like this:
var webClient = new WebClient();
var bytes = webClient.DownloadData(url);
var ms = new MemoryStream(bytes);


Where url is the path to the file. It can be more complicated using HttpWebRequest:
public ActionResult Export(string uri)
        {
            HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(uri);
            webRequest.Method = "GET";
            webRequest.KeepAlive = false;
            webRequest.PreAuthenticate = false;
            webRequest.Timeout = 1000;
            var response = webRequest.GetResponse();
            var stream = response.GetResponseStream();
            var previewIconSize = Config.IconSizes.FirstOrDefault(c => c.Name == "AvatarSize");
            var filePreviewPath = Path.Combine("/Content/files/previews", Guid.NewGuid().ToString("N") + ".jpg");
            if (previewIconSize != null)
            {
                PreviewCreator.CreateAndSavePreview(stream, new Size(previewIconSize.Width, previewIconSize.Height), Server.MapPath(filePreviewPath));
            }
            return Content("OK");
        }


Here the file is set through the generation of Guid.NewGuid. We check:
http://localhost/File/Export?uri=https://st.free-lance.ru/users/chernikov/upload/sm_f_81850beffd0d0c89.jpg

The file has downloaded and processed. Everything is super!

I recommend going through the PreviewCreator debugger to understand how everything works there.

All sources are located at https://bitbucket.org/chernikov/lessons

Also popular now: