ASP.NET MVC Урок 9. Configuration и загрузка файлов

от автора

Цель урока. Научиться использовать файл конфигурации Web.config. Application section, создание своих ConfigSection и IConfig. Научиться загружать файлы, использование file-uploader для загрузки файла, последующая обработка файла.

В этом уроке мы рассмотрим работу с конфигурационным файлом Web.config. Это xml-файл и в нем хранятся настройки программы.

Рассмотрим подробнее, из чего состоит этот файл:

  • configSection. Это секция отвечает за то, какие классы будут обрабатывать далее объявленные секции. Состоит из атрибута name — это тег, далее объявленной секции, и type – к какому классу относится.
  • connectionStrings. Это секция отвечает за работу с указанием строк инициализаций соединений с базами данных.
  • appSettings. Секция параметров типа key/value.
  • system.web, system.webServer. Секции параметров для работы веб-приложения.
  • runtime. Секция по настройке в режиме выполнения. Определение зависимостей между dll.
  • Остальные секции. Другие секции с параметрами, объявленными в configSection.

IConfig (и реализация).

Аналогично Repository, конфигуратор будем создавать как сервис. Создаем IConfig и Config-реализацию в папке Global (/Global/Config/IConfig.cs):

public interface IConfig     {         string Lang { get; }     } 

И

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

Добавляем строку в RegisterServices (/App_Start/NinjectWebCommon.cs):

 kernel.Bind<IConfig>().To<Config>().InSingletonScope(); 

И выводим в BaseController:

[Inject] public IConfig Config { get; set; } 


Теперь сделаем в инициализации контроллера переопеределение CultureInfo в потоке (/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);         } 

И добавим вывод даты в Index.cshtml (/Areas/Default/Views/Home/Index.cshtml):

    @DateTime.Now.ToString("D") 

Получаем вывод:

И по-настоящему свяжем это с Web.Config. Добавим в Web.config в appSettings строку:

<add key="Culture" value="ru" /> 

В Config.cs (/Global/Config/Config.cs):

public string Lang         {             get              { return ConfigurationManager.AppSettings["Culture"] as string;               }         } 

Запускаем – результат тот же, теперь изменим значение в Web.config на fr:

<add key="Culture" value="fr" /> 

Получаем дату:

mardi 5 mars 2013 

Отлично! Можете попробовать еще с несколькими языками. Список сокращений находится тут http://msdn.microsoft.com/en-us/goglobal/bb896001.aspx

Создание своих типов ConfigSection

В этой части мы рассмотрим создание своих собственных ConfigSection. В этой главе мы реализуем загрузку файлов и создание превью. Нам понадобятся следующие данные: во-первых, зависимость mime-type от расширения, и иконка файлов (для скачивания, например):

  • расширение
  • mime-type
  • большая иконка
  • маленькая иконка

и во-вторых, данные для создания превью:

  • наименование превью (например, UserAvatarSize)
  • ширина
  • высота

Оба типа делаются одинаково, так что я распишу только создание одного из них. Пусть это будет IconSize, для создания превью. Первое, что надо сделать — это создать класс, наследуемый 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"];             }         }     } 

Рассмотрим подробнее:

  • ConfigurationProperty состоит из имени, это имя атрибута в строке
  • IsRequired – обязательный этот параметр или нет
  • IsKey – является ли ключом (как первичный ключ в БД)
  • DefaultValue – значение по умолчанию

Следующий шаг – это создание класса коллекции (так как у нас будет множество элементов) и секции (/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;         } } 

В Web.config добавляем:

<iconConfig>     <iconSizes>       <add name="Avatar173Size" width="173" height="176" /> … </iconSizes> </iconConfig>  

Теперь необходимо объявить класс разбора этой секции в configSection:

      <section name="iconConfig" type="LessonProject.Global.Config.IconSizesConfigSection, LessonProject" /> 

Обратите внимание, что в описание type необходимо указать имя dll (LessonProject), в которой он содержится. Это важно, но будет рассмотрено в unit-тестах.

MailSettings

Создадим одиночный конфиг для настроек по работе с smtp-почтой. Нам понадобятся:

  • SmtpServer. Имя сервера.
  • SmtpPort. Порт, обычно 25й.
  • SmtpUserName. Логин.
  • SmtpPassword. Пароль.
  • SmtpReply. Обратный адрес в строке Reply-to.
  • SmtpUser. Имя пользователя в строке From.
  • EnableSsl. Да/нет, использовать ли работу по Ssl.

Файл (/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;             }         }     } 

Добавим в Web.config:

    <section name="mailConfig" type="LessonProject.Global.Config.MailSetting, LessonProject" /> 

И

  <mailConfig      SmtpServer="smtp.gmail.com"      SmtpPort="587"      SmtpUserName="lxndrpetrov"      SmtpPassword="**********"      SmtpReply="lxndrpetrov@gmail.com"      SmtpUser="test"     EnableSsl="true" />  

Добавим все это теперь в IConfig.cs и Сonfig.cs (/Global/Config/IConfig.cs):

public interface IConfig     {         string Lang { get; }          IQueryable<IconSize> IconSizes { get; }          IQueryable<MimeType> MimeTypes { get; }          MailSetting MailSetting { get; }     } 


И

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

Мы еще добавим MailTemplates — шаблоны которые нам понадобятся для рассылки email при регистрации, или при напоминании пароля.

Простая загрузка файлов

Сейчас рассмотрим стандартный пример загрузки файла на сервер, и больше никогда не будем пользоваться таким способом. Класс SimpleFileView для взаимодействия (/Models/Info/SimpleFileView.cs):

public class SimpleFileView     {         public HttpPostedFileBase UploadedFile { get; set; }     } 

Обратите внимание на наименование класса для приема файлов. Итак, создадим контроллер SimpleFileController (/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);         }     } 

И добавим View:

@model LessonProject.Models.Info.SimpleFileView @{     ViewBag.Title = "Index";     Layout = "~/Areas/Default/Views/Shared/_Layout.cshtml"; }  <h2>Index</h2>  @using (Html.BeginForm("Index", "SimpleFile", FormMethod.Post, new {enctype = "multipart/form-data", @class = "form-horizontal" })) {     <fieldset>         <div class="control-group">             <label class="control-label" for="Email">                 Загрузите файл:</label>             <div class="controls">                 @Html.TextBox("UploadedFile", Model.UploadedFile, new { type = "file", @class = "input-xlarge" })                 @Html.ValidationMessage("UploadedFile")             </div>         </div>         <div class="form-actions">             <button type="submit" class="btn btn-primary">                 Upload</button>         </div>     </fieldset> } 

Обратите внимание, на enctype в атрибутах формы и на type в атрибутах TextBox (на самом деле тип еще бывает password, checkbox, radio, но для них есть соответствующие методы в @Html-классе). Enctype необходимо установить в “multipart/form-data”, чтоб была возможность загрузить большой объём информации.

Загружаем и проверяем. Наш файл благополучно загружен, только необходимо сохранить InputStream в некий файл. Но оставим пока так и рассмотрим недостатки.

Первый недостаток – это то, что во всех браузерах форма выбора файла выглядит по-разному:

Конечно, ведь дизайнер представляет себе, что загрузка файлов выполняется как в Safari, а заказчик проверяет в Chrome и IE, и начинает спрашивать у разработчиков: «Что за самодеятельность?»
Второй недостаток –если форма не прошла валидацию, то эти поля необходимо выбрать заново. Т.е. есть такая форма:

  • Имя
  • Фамилия
  • Электронная почта
  • Дата рождения
  • Фотография
  • Фотография первого разворота паспорта
  • Фотография второго разворота паспорта
  • Фотография паспорта с пропиской
  • Пароль
  • Пароль еще раз
  • Капча

И вдруг вы набрали пароль неверно, или капчу не так ввели, или фотография второго разворота паспорта слишком большая, или вы забыли перегнать из raw-формата в jpeg.

В итоге фотографии, прописку и капчу надо вводить заново. Естественно, это совсем не user friendly, и раздражает заказчика (к тому же дизайнер нарисовал красиво, а выглядит убого).

Загрузка файла (ов) с помощью Ajax

Определим как должна вести себя загрузка файла:

  • Пользователь кликает на «загрузить».
  • Открывается форма выбора файла
  • Пользователь выбирает файл
  • Файл загружается, или выдается ошибка о том, что что-то не так
  • Если даже форма и не проходит валидацию, то файл остается загруженным и его не нужно загружать заново.

Это называется ajax-загрузка и для нее используем fineuploader (http://fineuploader.com/). Библиотека платная, но мы скачаем и соберем исходники (у нас же есть bundle!). Скачиваем исходники по ссылке: https://github.com/valums/file-uploader. Перемещаем js-файлы в папку /Scripts/fine-uploader. Css-файлы перемещаем в /Content и изображения в /Content/images. Перепишем правильно url в fineuploader.css для изображений:

.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; } 

Файлы инициализируем в 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")); 

Создаем контроллер 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});         }     } 

Метод-action Upload принимает строковое значение qqfile, я ниже рассмотрю, почему так. А сейчас создадим View для Index. Для этого:

  • Создаем кнопку, при нажатии на которую мы загружаем файл.
  • Файл загружается и создается превью
  • Файл и превью сохраняются в файловую систему
  • Метод возвращает ссылку, куда были загружены файл и превью, через Json-ответ
  • Если файлы не удалось загрузить, то выдается соответствующая ошибка
  • Обрабатываем json-результат и уведомляем, что файл и превью загружено
  • Верификация формы и запись в БД не нужны.

View для 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") }  <h2>Index</h2>  <fieldset>     <div class="control-group">         <label class="control-label" for="Text">             Image         </label>         <div class="controls">             <div id="UploadImage">                 Upload              </div>         </div>     </div>     <div>         <img src="" alt="" id="ImagePreview" />     </div> </fieldset> 

Наша кнопка с id=UploadImage. Добавляем file-index.js файл для обработки (/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(); });  

Теперь обработаем загрузку:

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 });         }  

В Content добавим папку files — это будет папка пользовательских данных. Разберем код:

  • Получаем qqfile (тут ничего не поменять, это параметр обусловлен fineuploader).
  • Из него получаем extension.
  • По extension находим mimeType. Для .jpg, .gif, .png – мы получаем mime-type типа «image/…». Таким образом, мы проверяем, что этот файл можно загрузить.
  • Далее, используя имя файла, составляем абсолютный путь к папке /Content/files (которую мы заранее создали) с помощью Server.MapPath.
  • Далее сохраняем файл с помощью SaveAs.
  • Возвращаем имя файл в json data.filePath.

Проверяем, всё ли загружается, и приступим к созданию превью.

Создание превью

Во-первых, мы немного схитрили с mime-type = «image\…», ведь к ним относится и bmp, и tiff файлы, которые не поддерживаются браузерами.
Так что создадим класс PreviewCreator в проекте LessonProject.Tools (PreviewCreator.cs):

   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;         }     } 

И заменим в FileController.cs (/Areas/Default/Controller/FileController.cs):

if (mimeType != null && PreviewCreator.SupportMimeType(mimeType.Name)) 

В PreviewCreator есть много функций для создания превью, так что я перечислю разные варианты создания изображения и подробно разберу один из них. Стоит учесть, что все превью создаются в формате jpeg. Итак, какие есть варианты:

  • Цветной и чернобелый вариант. Контролируется параметром grayscale (по умолчанию = false)
  • Превью. (CreateAndSavePreview) Если исходное изображение меньше, чем размеры превью, то изображение размещается посередине белого холста. Если по отношению к размерам исходный размер имеет вертикальную ориентированность (квадратик из портретного формата) – вырезаем верхнюю часть. Если же отношение горизонтально ориентированно относительно размера, то вырезаем середину.
  • Аватар. (CreateAndSaveAvatar) Если исходное изображение меньше, чем размеры превью, то изображение просто сохраняется. Если по отношению к размерам исходный размер имеет вертикальную ориентированность (квадратик из портретного формата) – то уменьшаем, по высоте. Если же отношение горизонтально ориентированно относительно размера, то вырезаем середину.
  • Изображение. (CreateAndSaveImage) Если изображение меньше, чем максимальные размеры, то сохраняем исходное. Если же изображение не вписывается в границы, то уменьшаем, чтобы оно не превышало максимальный размер, и сохраняем.
  • По размеру. (CreateAndSaveFitToSize) Если изображение меньше, чем размеры, то оно будет растянуто до необходимых размеров. С потерей качества, конечно же.
  • Обрезать. (CropAndSaveImage) Кроме стандартных параметров передаются координаты для обрезки изображения.

Cоздадим превью (CreateAndSavePreview), взяв из конфигурации размеры для создания превью AvatarSize (/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                         }                     }); 

Запускаем. Загружаем. Файлы должны загрузиться, и создастся превью.
Теперь сделаем обработку в 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);         }); 

теперь наш файл загружается вместе с превью. Путь большого файла также можно передавать отдельно, и записывать, например, в hidden поле и сохранять в дальнейшем в БД как строку.
Что плохого в такой конструкции, так это две следующие проблемы:

  • файлы могут быть перезаписаны, но это решается тем, что можно брать только расширение, а имя файлу присваивать отдельно, или добавлять немного соли
  • файлы могут быть загружены и не связаны с БД. Это можно решить тем, что для каждой таблице файлы записывать в отдельную папку, а потом делать поиск и удалять не записанные.
Получение файлов по ссылке

Есть еще один метод загрузки файла. Файл свободно болтается в интернете, а мы указываем путь к нему (например, при авторизации с facebook), а мы уже по ссылке сохраняем этот файл.
Это делается так:

var webClient = new WebClient(); var bytes = webClient.DownloadData(url); var ms = new MemoryStream(bytes); 

Где url – путь к файлу. Можно сложнее, с использованием 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");         } 

Тут файл задается через генерацию Guid.NewGuid. Проверяем:

http://localhost/File/Export?uri=https://st.free-lance.ru/users/chernikov/upload/sm_f_81850beffd0d0c89.jpg 

Файл загрузился и обработан. Всё супер!

Рекомендую пройтись дебаггером по работе PreviewCreator, чтобы понять, как там всё устроено.

Все исходники находятся по адресу https://bitbucket.org/chernikov/lessons

ссылка на оригинал статьи http://habrahabr.ru/post/176069/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *