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.