Как для потоковой передачи файлов с привязкой модели с помощью Ajax в ASP.NET CORE? - PullRequest
0 голосов
/ 12 апреля 2019

Насколько мне известно, в ASP.NET Core не существует очень простого способа потоковой передачи файлов с помощью Ajax, и все же привязка к модели представления для проверки ModelState.IsValid (используя Bootstrap, а не Angular)

Я знаю об этом Ссылка Microsoft , но пример "Загрузка больших файлов с потоковой передачей" предназначен для Angular, а не для Bootstrap.

Загрузка небольших файлов с использованием IFormFile работает, но это не такчто я ищу.

На стороне клиента:

let data = new FormData(document.forms[0]);

$.ajax({
    url: '@Url.Action("AjaxUpload", "MyController")',
    data: data,
    cache: false,
    contentType: false,
    processData: false,
    method: 'POST',
    headers: { 'RequestVerificationToken': '@GetAntiXsrfRequestToken()' },

    success: function (data) {
        alert('uploadStreamingFiles / success');
    },
    error: function (returned) {
        alert('uploadStreamingFiles / error');
    }
});

На стороне сервера в контроллере успешно вызывается действие AjaxUpload (), но привязка модели не выполняетсяTryUpdateModelAsync:

    [HttpPost]
    [DisableFormValueModelBinding]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> AjaxUpload()
    {
        if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
        {
            return BadRequest($"Expected a multipart request, but got {Request.ContentType}");
        }

        int fileNo = 0;
        var vm = new MyViewModel();

        // Used to accumulate all the form url encoded key value pairs in the request 
        var formAccumulator = new KeyValueAccumulator();
        string targetFilePath = null;

        var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(Request.ContentType), _defaultFormOptions.MultipartBoundaryLengthLimit);
        var reader = new MultipartReader(boundary, HttpContext.Request.Body);

        var section = await reader.ReadNextSectionAsync();
        while (section != null)
        {
            ContentDispositionHeaderValue contentDisposition;
            var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition);

            if (hasContentDispositionHeader)
            {
                //Get the view model data
                if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition))
                {
                    // Do not limit the key name length here because the 
                    // multipart headers length limit is already in effect.
                    var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name);
                    var encoding = GetEncoding(section);
                    using (var streamReader = new StreamReader(
                        section.Body,
                        encoding,
                        detectEncodingFromByteOrderMarks: true,
                        bufferSize: 1024,
                        leaveOpen: true))
                    {
                        // The value length limit is enforced by MultipartBodyLengthLimit
                        var value = await streamReader.ReadToEndAsync();
                        if (string.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase))
                        {
                            value = string.Empty;
                        }
                        formAccumulator.Append(key.ToString(), value);

                        Debug.WriteLine($"AjaxUpload / property posted: {key.ToString()}");

                        if (formAccumulator.ValueCount > _defaultFormOptions.ValueCountLimit)
                        {
                            throw new InvalidDataException($"Form key count limit {_defaultFormOptions.ValueCountLimit} exceeded.");
                        }
                    }
                }

                //Get the uploaded files
                else if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
                {
                    //ViewModel data should be passed first (hopefully?) so we can use this to determine where to save the file
                    fileNo += 1;
                    if (fileNo == 1)    //once the first file is passed, attempt to bind the ViewModel
                    {
                        var formValueProvider = new FormValueProvider(
                            BindingSource.Form,
                            new FormCollection(formAccumulator.GetResults()),
                            CultureInfo.CurrentCulture);

                        try
                        {
                            var bindingSuccessful = await TryUpdateModelAsync(vm, prefix: "", valueProvider: formValueProvider);

                            if (!bindingSuccessful)
                            {
                                if (!ModelState.IsValid)
                                {
                                    return BadRequest(ModelState);
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                           //TODO: On a small file of 377 bytes, TryUpdateModelAsync throws "Unexpected end of Stream, the content may have already been read by another component."
                        }
                    }

                    //TODO: Figure out how to load the full ViewModel before *any* upload is being saved (some properties appear to be streamed after the file uploads...)

                    targetFilePath = Path.GetTempFileName();    //TODO: later on this will need to be changed based on the ViewModel passed to the controller
                    using (var targetStream = System.IO.File.Create(targetFilePath))
                    {
                        await section.Body.CopyToAsync(targetStream);
                    }
                }
            }

            // Drains any remaining section body that has not been consumed and
            // reads the headers for the next section.
            section = await reader.ReadNextSectionAsync();
        }

        return Json(new { state = 100, message = "All saved" });
    }

Обратите внимание, что я вижу данные в объекте "formAccumulator", но они не привязаны к MyViewModel.Я мог бы пройтись по нему и заполнить MyViewModel, но это немного неуклюже ...

Вызов действия Ajax для потоковой загрузки файлов, и модель представления должна вести себя так же, как обычная не-AJAX-публикация, а именно:быть в состоянии проверить ModelState.IsValid на стороне сервера и использовать все свойства ViewModel.

...