В этом уроке мы рассмотрим работу с конфигурационным файлом 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/
Добавить комментарий