Цель урока. Научиться использовать файл конфигурации 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
Автор: chernikov