ASP.NET MVC: Привязка данных модели, которые содержат изображения

    Привязка данных(binding) является достаточно удобным средством ASP.NET MVC. Удобно оно в первую очередь тем, что позволяет скрыть реализацию преобразования данных между данными из модели и данными HTTP запроса.
    В своих проектах я часто сталкиваюсь с необходимостью сохранять различные данные типа blob и image. В этой статье я бы хотел показать, как можно легко организовать и использовать привязку данных из модели, которые содержат различные изображения. Для примера я взял учебный проект MVC Music Store и решил его подправить — добавить возможность изменять изображение обложки музыкального альбома. При написании данной статьи, я использовал версию APS.NET MVC 3 и Razor.


    Реализация привязки


    В общем, привязка данных предназначена для загрузки и сохранения данных модели. Реализуем класс, который кастомизирует привязку данных по умолчанию.
    public class ImageModelBinder : DefaultModelBinder
        {
            private string _fieldName;
            public ImageModelBinder(string fieldName)
            {
                _fieldName = fieldName;
            }
    
            public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
            {
                var obj = base.BindModel(controllerContext, bindingContext);
                ValueProviderResult valueResult;
                valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + "." + _fieldName);
                if (valueResult == null)
                {
                    valueResult = bindingContext.ValueProvider.GetValue(_fieldName);
                }
                if (valueResult != null)
                {
                    HttpPostedFileBase file = (HttpPostedFileBase)valueResult.ConvertTo(typeof(HttpPostedFileBase));
                    if (file != null)
                    {
                        byte[] tempImage = new byte[file.ContentLength];
                        file.InputStream.Read(tempImage, 0, file.ContentLength);
                        PropertyInfo imagePoperty = bindingContext.ModelType.GetProperty(_fieldName);
                        imagePoperty.SetValue(obj, tempImage, null);
                    }
                }
                return obj;
            }
        }
    

    В этой реализации мы переопределили основной метод BindModel, вызвали сначала базовую реализацию для привязки всех данных, а затем реализовали конвертацию и запись в поле данных изображения из поля формы HTTP запроса типа HttpPostedFileBase.
    Так же, было бы неплохо создать свой атрибут:
    public class ImageBindAttribute : CustomModelBinderAttribute
        {
            private IModelBinder _binder;
            public ImageBindAttribute(string fieldName)
            {
                _binder = new ImageModelBinder(fieldName);
            }
            public override IModelBinder GetBinder() { return _binder; }
        }
    


    Для создания вида нам также пригодится простой хелпер создания поля загрузки файла:
    public static IHtmlString ImageUpload(this HtmlHelper helper, string name)
            {
                return ImageUpload(helper, name, null);
            }
    
            public static IHtmlString ImageUpload(this HtmlHelper helper, string name, object htmlAttributes)
            {
                var tagBuilder = new TagBuilder("input");
                tagBuilder.GenerateId(name);
                UrlHelper urlHelper = new UrlHelper(helper.ViewContext.RequestContext);
    
                tagBuilder.Attributes["name"] = name;
                tagBuilder.Attributes["type"] = "file";
                tagBuilder.MergeAttributes(new RouteValueDictionary(htmlAttributes));
    
                return MvcHtmlString.Create(tagBuilder.ToString());
            }
    


    Использование


    Теперь модифицируем основной проект.
    В нашем проекте есть модель — класс Album. Добавим новое поле Image типа byte[]. Так же исключим это новое поле из стандартной привязки.
    [Bind(Exclude = "AlbumId, Image")]
        public class Album
        {
    	...
            public string AlbumArtUrl { get; set; }
    
            [ScaffoldColumn(false)]
            public byte[] Image { get; set; }
            ...
        }
    

    И не забудем добавить новое поле в базу.

    Далее нам нужно каким-то образом декларировать нашу привязку. Это можно сделать, добавив атрибут в соответствующий Action, либо добавив привязку данных глобально.
    public ActionResult Create([ImageBind("Image")] Album album)
            {
                if (ModelState.IsValid)
                {
                    storeDb.Albums.Add(album);
    


    К сожалению это не подойдет для метода Edit, где используется тип FormCollection
    в аргументах. Тогда можно просто добавить новую привязку в коллекцию, например так:
     public ActionResult Edit(int id, FormCollection collection)
            {
                var album = storeDb.Albums.Find(id);
                if (Binders[typeof(Album)] == null) Binders.Add(typeof(Album), new ImageModelBinder("Image"));
    ...
    


    Либо сделать это глобально, в файле Global.asax:
    ModelBinders.Binders.Add(typeof(Album), new ImageModelBinder("Image"));
    


    Для загрузки изображений, модифицируем вид и добавим поле в форму:
    <div class="editor-field">
                @Html.ImageUpload("Image")
    </div>
    

    и изменим вызов хелпера формы, добавив новый HTML атрибут enctype = «multipart/form-data» для возможности аплодинга бинарных данных:
    (Html.BeginForm("Edit", "StoreManager", FormMethod.Post, new { enctype = "multipart/form-data" }))
    

    Вот примерно и все по поводу загрузки и сохранения.

    Для отображения нам необходимо создать новый Action.
    [OutputCache(Duration = 0)]
    public ActionResult Image(int id)
            {
                var album = storeDb.Albums.Find(id);
                return new FileStreamResult(new MemoryStream(album.Image), "image/png");
            }
    

    В этом примере, ограничимся использованием изображения png формата. Также нужно позаботиться о кешировании, указав соответствующий атрибут.

    Для создания вида, нам пригодится простой хелпер:
    public static IHtmlString Image(this HtmlHelper helper, string name, string id)
            {
                return Image(helper, name, id, null);
            }
    
            public static IHtmlString Image(this HtmlHelper helper, string name, string id, object htmlAttributes)
            {
                var tagBuilder = new TagBuilder("img");
                UrlHelper urlHelper = new UrlHelper(helper.ViewContext.RequestContext);
    
                tagBuilder.Attributes["src"] = urlHelper.Action(name, null, new { id = id });
                tagBuilder.Attributes["alt"] = string.Format("{0} of {1}", name, id);
                tagBuilder.MergeAttributes(new RouteValueDictionary(htmlAttributes));
    
                return MvcHtmlString.Create(tagBuilder.ToString());
            }
    


    А в самом виде добавим.
    <div class="field">
                @Html.Image("Image", @Model.AlbumId.ToString())
            </div>
    


    Заключение


    На этом примере я хотел показать, как можно быстро и легко организовать привязку данных в MVC. Как можно видеть, в результате использования привязки, код реализации вида, модели и контроллера получается достаточно простой и лаконичный. А самое главное то, что один раз реализовав привязку, мы можем использовать это где либо еще.

    Что можно улучшить.
    Добавить хелпер BeginForm — сокращенный вариант, для того, чтобы не писать имена контроллера и действия. Реализация Action для отображения достаточно примитивна. По-хорошему, нужно сохранять тип изображения и корректно его возвращать и реализовать валидацию банарных данных. А вместо хелперов, которые я привел в этой статье, более правильный метод это использовать шаблоны редактирования и просмотра. Можно также использовать такое мощное средство, как фильтры.

    Источники


    mvcmusicstore.codeplex.com
    www.highoncoding.com/Articles/689_Uploading_and_Displaying_Files_Using_ASP_NET_MVC_Framework.aspx
    www.hanselman.com/blog/SplittingDateTimeUnitTestingASPNETMVCCustomModelBinders.aspx
    odetocode.com/Blogs/scott/archive/2009/04/27/6-tips-for-asp-net-mvc-model-binding.aspx
    odetocode.com/blogs/scott/archive/2009/05/05/iterating-on-an-asp-net-mvc-model-binder.aspx
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 7

      0
      Хорошее оформление примера, чётко и понятно, положу к себе в «How to».
        +2
        string.Format("{0}/{1}", urlHelper.Action(name), id) — не очень хорошо.
          0
          Большое спасибо за замечание, исправил.
          +2
          Для создания вида, нам пригодиться простой хелпер:

          возможно было бы удобнее свой DisplayTemplate реализовать.
            0
            Ну я об этом написал в заключении. Хотя, нужно сказать, что одно другого не исключает, и можно использовать их совместно. Хелперы удобны тем, что их можно собрать и вынести в отдельную сборку. У шаблонов же другие преимущества. На счет примера, приведенного в этой статье, я посчитал, что хелпер будет более проще реализовать, и к тому же, я не смог найти сразу красивое решения с шаблонами. Проблема заключается в том, что нужно передавать не само поле данных а другое — ид-шник а так же название акшена. Ну в общем попробуйте реализовать по простому, особенно для случая если у вас два поля с изображениями. Если у вас есть готовое решение — приведите тут, в комментарии, будет интересно посмотреть.
            0
            «К сожалению это не подойдет для метода Edit, где используется тип FormCollection
            в аргументах.»

            Тут вопрос в том, зачем вы вообще используете FormCollection. Это рассадник magic strings, да еще и зависимость от HttpContext. Плюс, это как раз то место, где нужно использовать ModelBinder'ы. Сделайте входящий Image частью, скажем, AlbumEditModel и получайте экземпляр AlbumEditModel в качестве параметра вместо FormCollection.
              0
              Это не я использую — это исходный код туториала. И по поводу использования FormCollection — согласен: это порочная практика. Но, этот случай позволил показать, как можно использовать привязку в нестандартных ситуациях.

            Only users with full accounts can post comments. Log in, please.