Многие начинающие программисты C# ASP .NET MVC (далее mvc) сталкиваются с задачей отправки данных с помощью Ajax. Вот только на практике эта задача оказывается не такой легкой.

На своей работе я пытаюсь придерживаться определённых принципов разработки программного обеспечения. Одним из них является минимизация написания кода и создание универсальный классов и функций. Именно этот принцип предполагал использование jquery.unobtrusive-ajax.js и класс Ajax для mvc.

В этой статье нас интересует непосредственно Ajax.BeginForm.

Пример:

@using (Ajax.BeginForm("UploadAvatarImage", "Dashboard", null,
 new AjaxOptions {HttpMethod = "POST", 
OnSuccess = "UpdateAvatars()", 
OnFailure = "document.refresh()" }, 
new {Id = "UploadAvatartForm", enctype = "multipart/form-data" }))
{
<div class="input-group pt-1 pl-1">
 <input type="file" name="avatarFile" class="form-control" id="fileUploaderControl" accept=".jpg"/>
</div>
<button class="btn btn-sm btn-primary btn-block m-1" type="submit" >Изменить</button>
}

Столкнувшись с тем, что Ajax.BeginForm не передает input[type=file], я провел глобальный поиск по нахождению решения данной проблемы. Старый любимый stackoverflow на каждом решение предлагал одно и то же. Не использовать Ajax.BeginForm, использовать FormData, сделать php обработчик и еще тучи советов по увеличению программного кода.

Как я говорил ранее, в своих продуктах я придерживаюсь принципов и раз ответ, который бы удовлетворял мои требования, не был найден, пришлось всё делать самому.

Первое, что потребовалось, это определить какая функция в jquery.unobtrusive-ajax.js отвечает за формирование data.

$(document).on("submit", "form[data-ajax=true]", function (evt) {
        var clickInfo = $(this).data(data_click) || [],
            clickTarget = $(this).data(data_target),
            isCancel = clickTarget && (clickTarget.hasClass("cancel") || clickTarget.attr('formnovalidate') !== undefined);
        evt.preventDefault();
        if (!isCancel && !validate(this)) {
            return;
        }
        asyncRequest(this, {
            url: this.action,
            type: this.method || "GET",
// То что нас интересует
            data: clickInfo.concat($(this).serializeArray())
        });
    });

Нас интересует функция $(this).serializeArray(). В переменной $(this) приходит наша форма <form/> и затем сереализуется в массив. Протестировав её в консоли, а так же её аналог serialize() было определено, что данные функции не загружают файлы в принципе. Отсюда и решение переписать её.

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

$(document).on("change", "form[data-ajax=true] input[type=file]", function (evt) {
        var form = $($(evt.target).parents("form")[0]);
        if (evt.target.files.length > 0) {
            var fileObj = evt.target.files[0];
            var reader = new FileReader();
            reader.onload = function () {
                evt.target.setAttribute("data-ajax-image-data", reader.result);
                form.find("button[type=submit]")[0].classList.remove("disabled");
            }
            reader.onerror = function () {
                console.log("Error while loading");
                form.find("button[type=submit]")[0].classList.remove("disabled");
            }
            form.find("button[type=submit]")[0].classList.add("disabled");
            reader.readAsDataURL(fileObj);
        }
    });

Немного о коде.

Сначала привязываем событие изменение input-а, выбор пал на «change».

Затем проверяем, выбран ли файл.

Если файл выбран то начинаем его загрузку. Проблема состояла в том, что данные надо где то хранить, в примере мы запишем данные в атрибут самого объекта.

Блокируем кнопку подтверждения до момента загрузки файла.

В случаи ошибки, пишем это в консоль. Это не окончательный вариант функции, каждый может модернизировать её под свои нужды.

После того, как мы создали загрузку данных файла, переходим к модернизации сериализации.

function selfFormSerializeArray(form) {
        var array = form.serializeArray();
        for (var i = 0; i < form[0].length; i++) {
            if (form[0][i].type === "file") {
                var fileObj = form[0][i];
                var data = fileObj.getAttribute("data-ajax-image-data") || "";
                if (data !== "") {
                    array.push({ name: fileObj.name, value: data });
                   // console.log("SUCCESS");
                } else {
                    //console.log("ERROR");
                }
            }
        }
        return array;
    }

И используем эту функцию в основной функции.

$(document).on("submit", "form[data-ajax=true]", function (evt) {
        var clickInfo = $(this).data(data_click) || [],
            clickTarget = $(this).data(data_target),
            isCancel = clickTarget && (clickTarget.hasClass("cancel") || clickTarget.attr('formnovalidate') !== undefined);
        evt.preventDefault();
        if (!isCancel && !validate(this)) {
            return;
        }
        asyncRequest(this, {
            url: this.action,
            type: this.method || "GET",
            data: clickInfo.concat(selfFormSerializeArray($(this))) // clickInfo.concat($(this).serializeArray())
        });
    });

По завершению требуется дописать обработчик и парсер данных. На скорую руку это выглядет приблизительно так.

Обработчик

public PartialViewResult UploadAvatarImage()
        {
            if (!ImageHelper.LoadAvatarImage(this.Request.Form["avatarFile"]))
            {
                return null;
            }
            return PartialView("Null");
        }

Хелпер

public static bool LoadAvatarImage(string data)
        {
            try
            {
                var file = AjaxFile.Create(data);
                return true;
            }
            catch (Exception ex)
            {
                return false;
            }

Парсер

public AjaxFile(string data)
        {
            var infos = data.Split(',');
            this.Type = infos[0].Split(';')[0].Split(':')[1].Split('/')[1];
            this.Source = Convert.FromBase64String(infos[1]); ;
        }
        
        public static AjaxFile Create(string data)
        {
            return new AjaxFile(data);
        }

Поздравляю! Теперь вы можете загружать файлы использую стандартный Ajax для mvc.

Комментарии (22)


  1. Veikedo
    15.09.2018 16:35

    Не использовать Ajax.BeginForm, использовать FormData, сделать php обработчик и еще тучи советов по увеличению программного кода.

    Думаю, стоило прислушаться к совету и не использовать Ajax.BeginForm.
    Кода получилось бы меньше, да и получился бы он более поддерживаемым. А с учётом кучи JS библиотек, задачу вы решили бы быстрее.


    Да и логику вашего обработчика/хелпера я так и не понял.


    1. freerefill Автор
      15.09.2018 23:17

      22 строчки JS кода, и любой /> теперь поддерживает отправку.

      Обработчик, Определяет тип фала, по нему создаёт .* type после чего я могу с помощь File.Open сделать запись (что и так приходиться делать при передачи файлов в asp .net да же с помощь классики.
      Зачем куча JS если 2 функции заменяют нужное решение?


  1. catsmile
    15.09.2018 18:50

    this.Type = infos[0].Split(';')[0].Split(':')[1].Split('/')[1];

    Вот за подобные вещи я на код-ревью бью по рукам. Не делайте так.


    1. freerefill Автор
      15.09.2018 23:22

      Ну я написал, что на скорую руку.
      По факту это выглядит намного по другому.
      Помимо типо я вытаскиваю формат в котором зашифрован файл, общий формат и его расширение.
      Тема в статье основная это передача через Ajax. Фулл-код развертку выкидывать нету смысла.


    1. Hayao
      15.09.2018 23:22

      А что не так? В парсере где надо вылететь сразу если структура не соответствует ожидаемой


      1. freerefill Автор
        15.09.2018 23:22

        Что тоже верно, ведь в helper-е накинут Try-catch


      1. lair
        17.09.2018 13:46

        А то, что попробуйте понять, что пошло не так, по очень понятной ошибке IndexOutOfRangeException.


  1. lair
    16.09.2018 01:36

    Подождите, то есть вы серьезно льете файлы на сервер в base64? Про multipart и прочее вы никогда не слышали?


    Извините, но на универсальное (и тем более — качественное) решение это никак не тянет.


    1. freerefill Автор
      16.09.2018 07:04

      То есть вы всерьез предлагаете, помимо js, c# и прочих прелестей asp .net привязать PHP
      HabrHabr или QT?


      1. Veikedo
        16.09.2018 11:15

        Вы же понимаете, что multipart это стандарт и никак не привязан к технологии?


        1. freerefill Автор
          16.09.2018 17:35

          Знаете, каждому своё, один использует base64 который его устраивает,
          другой может использовать readAsBinaryString()


          1. lair
            17.09.2018 02:13

            … и что вы дальше будете с этим бинарником делать? Вы, гм, вообще понимаете, зачем мультипарт нужен?


          1. yarosroman
            17.09.2018 05:44

            Увеличение размера запроса на треть не смущает, хорошо если вы сами можете настроить размер запроса, а где нибудь на хостинге может стать проблемой.


            1. freerefill Автор
              17.09.2018 08:49

              Работали вообще с ASP .net?
              IIS настроили, web.config настроили, если уж очень требовательны к размеру запроса, то ограничили его в js, 2 строчками кода, получив длину загружаемого файла.


              1. yarosroman
                17.09.2018 12:19

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


      1. lair
        16.09.2018 12:48

        Нет, я предлагаю использовать multipart, который asp.net прекрасно поддерживается.


        (Вам ведь не зря советовали взять FormData)


        1. freerefill Автор
          16.09.2018 17:37

          StackOverflow
          Все эти решения и им подобные на Stack-е предлагают одно и то же, обработчик привязывать к форме и каждый раз писать его, что в моём случаи больше не требуется.


          1. lair
            17.09.2018 02:11

            Ну то есть самостоятельно, без StackOverflow, вы не понимаете, как написать такой обработчик, который не надо было бы каждый раз писать, и который был работал с FormData? Маленький намек: это можно сделать даже в вашем обработчике (если не учитывать остальные его недостатки); кода, что характерно, будет намного меньше после этого, ваш onchange можно будет выкинуть полностью.


  1. lair
    16.09.2018 12:58

    И используем эту функцию в основной функции.

    Я тут немножко не понял. Вы правите код функции, поставляющейся с jquery.unobtrusive-ajax? Или пишете свою функцию, которую вешаете на тот же набор условий?


    1. freerefill Автор
      16.09.2018 17:34

      Вместо использование serializeArray() который не обрабатывает input [type=file] написали свою функцию и подменили её в файле с jquery.unobtrusive-ajax, для того, что бы он мог обрабатывать input не нарушая логику своей работы и не требовать дополнительных обработчиков на формы в дальнейшем.


      1. Drag13
        16.09.2018 18:21

        Отлично, вы сломали апдейт версий jquery.unobtrusive. По крайней попробуйте не подменять ее физически а «перезаписать»


      1. lair
        17.09.2018 02:06

        Я и говорю: правите код функции, поставляющейся third-party. Вы знаете, чем это грозит?