0

I'm having trouble to update the image of an author in my application. When I add the input field for files in my Razor pages the code doesn't jump to the OnPost method in the code behind. Without the file input field it does.

Below my code.

Razor page code:

@page
@model PersonalLibrary.Razor.Pages.Authors.EditModel
@{
}

<div class="container">
    <h1>Edit</h1>
    <h4>Author</h4>
    <form method="post">
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <input type="hidden" asp-for="Author.Id" />
        <div class="form-group">
            <img src="@Html.DisplayFor(model => model.Author.Image)" />
            <div class="file-field input-field">
                <div class="btn">
                    <span>Browse</span>
                    <input asp-for="Author.Image" type="file" />
                </div>
                <div class="file-path-wrapper">
                    <input asp-for="Author.Image" class="file-path validate" type="text" placeholder="Upload file" />
                </div>
            </div>
        </div>
        <div class="form-group">
            <label asp-for="Author.Name" class="control-label"></label>
            <input asp-for="Author.Name" class="form-control" />
            <span asp-validation-for="Author.Name" class="text-danger"></span>
        </div>
        <div class="form-group">
            <label asp-for="Author.Country" class="control-label"></label>
            <input asp-for="Author.Country" class="form-control" />
            <span asp-validation-for="Author.Country" class="text-danger"></span>
        </div>
        <div class="form-group">
            <label asp-for="Author.DateOfBirth" class="control-label"></label>
            <input asp-for="Author.DateOfBirth" class="datepicker form-control" />
            <span asp-validation-for="Author.DateOfBirth" class="text-danger"></span>
        </div>
        <div class="form-group">
            <input type="submit" value="Update" class="btn btn-primary" />
        </div>
    </form>
</div>

OnPost in the code behind:

public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }
            IFormFile image = (IFormFile)Author.Image;
            var imageUri = $"{baseUri}/authors/{Author.Id}/images";
            await WebApiClient.PutCallApi<Author, IFormFile>(imageUri, image);

            var uri = $"{baseUri}/authors/{Author.Id}";
            await WebApiClient.PutCallApi<Author, Author>(uri, Author);

            return RedirectToPage("./Index");
        }

Api controller methods:

[HttpPut("{id}")]
        public async Task<IActionResult> PutAsync(AuthorRequestDto authorRequestDto)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var authorResponseDto = await _authorService.UpdateAsync(authorRequestDto);
            return Ok(authorResponseDto);
        }

[HttpPost("{id}/image"), HttpPut("{id}/image")]
        public async Task<IActionResult> Image([FromRoute] Guid id, IFormFile image)
        {
            var author = await _authorService.AddOrUpdateImageAsync(id, image);

            if (author == null)
            {
                return NotFound($"Author with ID {id} not found");
            }

            return Ok(author);
        }

Any help welcome!

UPDATE

In the Edit.cshtml.cs file I've added a new BindProperty for the image.

namespace PersonalLibrary.Razor.Pages.Authors
{
    public class EditModel : PageModel
    {
        private readonly string baseUri = Constants.ApiBaseUri.BaseUri;
        [BindProperty]
        public Author Author { get; set; }
        [BindProperty]
        public IFormFile Image { get; set; }

        public async Task<IActionResult> OnGetAsync(Guid id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var uri = $"{baseUri}/authors/{id}";
            Author = await WebApiClient.GetApiResult<Author>(uri);

            return Page();
        }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            var imageUri = $"{baseUri}/authors/{Author.Id}/image";
            await WebApiClient.PutCallApi<IFormFile, IFormFile>(imageUri, Image);

            //var uri = $"{baseUri}/authors/{Author.Id}";
            //await WebApiClient.PutCallApi<Author, Author>(uri, Author);

            return RedirectToPage("./Index");
        }
    }
}

I had to change the enctype for the form to "multipart/form-data" in the Edit.cshtml file, like shown below.

@page
@model PersonalLibrary.Razor.Pages.Authors.EditModel
@{
}

<div class="container">
    <h1>Edit</h1>
    <h4>Author</h4>
    <form method="post" enctype="multipart/form-data">
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <input type="hidden" asp-for="Author.Id" />
        <div class="form-group">
            <img src="@Html.DisplayFor(model => model.Author.Image)" />
            <div class="file-field input-field">
                <div class="btn">
                    <span>Browse</span>
                    <input asp-for="Image" type="file" />
                </div>
                <div class="file-path-wrapper">
                    <input class="file-path validate" type="text" 
                     placeholder="Upload file" />
                </div>
            </div>
        </div>
       
        <div class="form-group">
            <input type="submit" value="Update" class="btn btn-primary" />
        </div>
    </form>
</div>

So now the Image property is filled with the image I want to upload, but it crashes when accessing the WebApiClient class.

namespace PersonalLibrary.Web.Helpers
{
    public class WebApiClient
    {
        private static JsonMediaTypeFormatter GetJsonFormatter() {
            var formatter = new JsonMediaTypeFormatter();
            //prevent self-referencing loops when saving Json (Bucket -> BucketItem -> Bucket -> ...)
            formatter.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
            return formatter;
        }

        public async static Task<T> GetApiResult<T>(string uri)
        {
            using (HttpClient httpClient = new HttpClient())
            {
                string response = await httpClient.GetStringAsync(uri);
                return JsonConvert.DeserializeObject<T>(response, GetJsonFormatter().SerializerSettings);
            }
        }

        public static async Task<TOut> PutCallApi<TOut, TIn>(string uri, TIn entity)
        {
            return await CallApi<TOut, TIn>(uri, entity, HttpMethod.Put);
        }

        public static async Task<TOut> PostCallApi<TOut, TIn>(string uri, TIn entity)
        {
            return await CallApi<TOut, TIn>(uri, entity, HttpMethod.Post);
        }

        public static async Task<TOut> DeleteCallApi<TOut>(string uri)
        {
            return await CallApi<TOut, object>(uri, null, HttpMethod.Delete);
        }

        private static async Task<TOut> CallApi<TOut, TIn>(string uri, TIn entity, HttpMethod httpMethod)
        {
            TOut result = default;

            using (HttpClient httpClient = new HttpClient())
            {
                HttpResponseMessage response;
                if (httpMethod == HttpMethod.Post)
                {
                    response = await httpClient.PostAsync(uri, entity, GetJsonFormatter());
                }
                else if (httpMethod == HttpMethod.Put)
                {
                    response = await httpClient.PutAsync(uri, entity, GetJsonFormatter());
                }
                else
                {
                    response = await httpClient.DeleteAsync(uri);
                }
                result = await response.Content.ReadAsAsync<TOut>();
            }
            return result;
        }
    }
}

This is the error I'm receiving:

UnsupportedMediaTypeException: No MediaTypeFormatter is available to read an object of type 'IFormFile' from content with media type 'application/problem+json'. System.Net.Http.HttpContentExtensions.ReadAsAsync(HttpContent content, Type type, IEnumerable formatters, IFormatterLogger formatterLogger, CancellationToken cancellationToken)

My guess is the problem is I use a JsonMediaTypeFormatter. But I can't figure out what to use to handle files or images.

  • 1
    1) your form tag is missing the enctype attribute required for uploading files (see any HTML forms file upload tutorial), 2) you've got two form input elements with asp-for="Author.Image"...one's a file input and one's a text input. This makes absolutely no sense and I don't know what you're trying to achieve. Are you perhaps wanting to allow the user to set a custom file name different from the real one which gets uploaded? If so you'll need a separate property in the viewmodel to store that. – ADyson Dec 09 '20 at 11:28
  • Yes, the file name for the image should have the same name as the Id of the author. I will try using a separate property. Thanks for the advice! – Hannes Derynck Dec 09 '20 at 12:00
  • You've already got a property for Author.Id, so you don't need another one as well! – ADyson Dec 09 '20 at 13:24
  • I've added a separate property for the image and set the enctype for the form to "multipart/form-data". When hitting the submit button the property gets filled, but I'm still having trouble with to complete the put request. I was using a JsonMediaTypeFormatter, beacause on other pages it's just text that needed to be posted. What type of MediaTypeFormatter should I use to post files or images? – Hannes Derynck Dec 23 '20 at 14:51
  • having trouble how exactly? Please be specific about what error / problem is occuring. And update the post to show your current code. Let's not make assumptions about formatters until we understand the exact issue. – ADyson Dec 24 '20 at 09:49
  • I've updated the original post with my new code and the error I'm receiving. – Hannes Derynck Dec 27 '20 at 11:39
  • Ah so it's not the postback from the form to the razor page method which fails now,it's the secondary call to your other API. That makes it a bit clearer. Yes you can't use JSON to post an image. What does the Web API actually accept? Is it your API or did someone else write it? – ADyson Dec 27 '20 at 16:20
  • The web API can accept images. I have tested the method in my controller with postman. Do you have any suggestion what I can use instead of JSON? I have written my own API based on the course I've got from school. – Hannes Derynck Dec 28 '20 at 17:10
  • Yes it can accept images obviously, but I meant what data format does it accept in the HTTP request? E.g. is it multipart form data, perhaps? You should know, if you've managed to get it working in PostMan (and if it was you who wrote the API). – ADyson Dec 28 '20 at 17:16
  • P.S. PostMan can actually auto-generate C# code (which uses the RestSharp library, arguably a bit more sophisticated than HttpClient) based on a PostMan request. – ADyson Dec 28 '20 at 17:18
  • The API accepts IFormFile. – Hannes Derynck Dec 29 '20 at 18:06
  • Does this answer your question? [How to post form-data IFormFile with HttpClient?](https://stackoverflow.com/questions/55412899/how-to-post-form-data-iformfile-with-httpclient) – ADyson Dec 29 '20 at 18:23

0 Answers0