Pull to refresh

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

Reading time 14 min
Views 51K
Цель урока. Научиться использовать файл конфигурации 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
Tags:
Hubs:
+43
Comments 1
Comments Comments 1

Articles