2020-12-11 14:55:19 +11:00
|
|
|
using System;
|
2020-05-08 13:23:44 +02:00
|
|
|
using System.Text;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
|
|
|
|
using Umbraco.Core;
|
|
|
|
|
using Umbraco.Core.Models.PublishedContent;
|
2020-12-10 18:09:32 +11:00
|
|
|
using Umbraco.Web.Common.Routing;
|
2020-05-08 13:23:44 +02:00
|
|
|
using Umbraco.Web.Models;
|
|
|
|
|
|
|
|
|
|
namespace Umbraco.Web.Common.ModelBinders
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
2020-12-11 14:55:19 +11:00
|
|
|
/// Maps view models, supporting mapping to and from any <see cref="IPublishedContent"/> or <see cref="IContentModel"/>.
|
2020-05-08 13:23:44 +02:00
|
|
|
/// </summary>
|
|
|
|
|
public class ContentModelBinder : IModelBinder
|
|
|
|
|
{
|
2020-12-11 14:55:19 +11:00
|
|
|
/// <inheritdoc/>
|
2020-05-08 13:23:44 +02:00
|
|
|
public Task BindModelAsync(ModelBindingContext bindingContext)
|
|
|
|
|
{
|
2020-12-10 18:09:32 +11:00
|
|
|
// Although this model binder is built to work both ways between IPublishedContent and IContentModel in reality
|
2020-12-11 14:55:19 +11:00
|
|
|
// only IPublishedContent will ever exist in the request so when this model binder is used as an IModelBinder
|
|
|
|
|
// in the aspnet pipeline it will really only support converting from IPublishedContent which is contained
|
|
|
|
|
// in the UmbracoRouteValues --> IContentModel
|
2020-12-10 18:09:32 +11:00
|
|
|
if (!bindingContext.ActionContext.RouteData.Values.TryGetValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken, out var source)
|
|
|
|
|
|| !(source is UmbracoRouteValues umbracoRouteValues))
|
2020-05-08 13:23:44 +02:00
|
|
|
{
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-11 14:55:19 +11:00
|
|
|
BindModel(bindingContext, umbracoRouteValues.PublishedContent, bindingContext.ModelType);
|
2020-05-08 13:23:44 +02:00
|
|
|
return Task.CompletedTask;
|
2020-10-19 14:03:51 +02:00
|
|
|
}
|
2020-05-08 13:23:44 +02:00
|
|
|
|
|
|
|
|
// source is the model that we have
|
|
|
|
|
// modelType is the type of the model that we need to bind to
|
|
|
|
|
//
|
|
|
|
|
// create a model object of the modelType by mapping:
|
|
|
|
|
// { ContentModel, ContentModel<TContent>, IPublishedContent }
|
|
|
|
|
// to
|
|
|
|
|
// { ContentModel, ContentModel<TContent>, IPublishedContent }
|
2020-12-11 14:55:19 +11:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Attempts to bind the model
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void BindModel(ModelBindingContext bindingContext, object source, Type modelType)
|
2020-05-08 13:23:44 +02:00
|
|
|
{
|
|
|
|
|
// Null model, return
|
|
|
|
|
if (source == null)
|
|
|
|
|
{
|
2020-12-11 14:55:19 +11:00
|
|
|
return;
|
2020-05-08 13:23:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If types already match, return
|
2020-12-11 14:55:19 +11:00
|
|
|
Type sourceType = source.GetType();
|
|
|
|
|
if (sourceType.Inherits(modelType))
|
2020-05-08 13:23:44 +02:00
|
|
|
{
|
2020-10-19 14:03:51 +02:00
|
|
|
bindingContext.Result = ModelBindingResult.Success(source);
|
2020-12-11 14:55:19 +11:00
|
|
|
return;
|
2020-05-08 13:23:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to grab the content
|
|
|
|
|
var sourceContent = source as IPublishedContent; // check if what we have is an IPublishedContent
|
|
|
|
|
if (sourceContent == null && sourceType.Implements<IContentModel>())
|
2020-12-11 14:55:19 +11:00
|
|
|
{
|
2020-05-08 13:23:44 +02:00
|
|
|
// else check if it's an IContentModel, and get the content
|
|
|
|
|
sourceContent = ((IContentModel)source).Content;
|
2020-12-11 14:55:19 +11:00
|
|
|
}
|
|
|
|
|
|
2020-05-08 13:23:44 +02:00
|
|
|
if (sourceContent == null)
|
|
|
|
|
{
|
|
|
|
|
// else check if we can convert it to a content
|
2020-12-11 14:55:19 +11:00
|
|
|
Attempt<IPublishedContent> attempt1 = source.TryConvertTo<IPublishedContent>();
|
2020-12-10 18:09:32 +11:00
|
|
|
if (attempt1.Success)
|
2020-12-11 14:55:19 +11:00
|
|
|
{
|
2020-12-10 18:09:32 +11:00
|
|
|
sourceContent = attempt1.Result;
|
2020-12-11 14:55:19 +11:00
|
|
|
}
|
2020-05-08 13:23:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we have a content
|
|
|
|
|
if (sourceContent != null)
|
|
|
|
|
{
|
|
|
|
|
// If model is IPublishedContent, check content type and return
|
|
|
|
|
if (modelType.Implements<IPublishedContent>())
|
|
|
|
|
{
|
|
|
|
|
if (sourceContent.GetType().Inherits(modelType) == false)
|
|
|
|
|
{
|
|
|
|
|
ThrowModelBindingException(true, false, sourceContent.GetType(), modelType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bindingContext.Result = ModelBindingResult.Success(sourceContent);
|
2020-12-11 14:55:19 +11:00
|
|
|
return;
|
2020-05-08 13:23:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If model is ContentModel, create and return
|
|
|
|
|
if (modelType == typeof(ContentModel))
|
|
|
|
|
{
|
|
|
|
|
bindingContext.Result = ModelBindingResult.Success(new ContentModel(sourceContent));
|
2020-12-11 14:55:19 +11:00
|
|
|
return;
|
2020-05-08 13:23:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If model is ContentModel<TContent>, check content type, then create and return
|
|
|
|
|
if (modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(ContentModel<>))
|
|
|
|
|
{
|
2020-12-11 14:55:19 +11:00
|
|
|
Type targetContentType = modelType.GetGenericArguments()[0];
|
2020-05-08 13:23:44 +02:00
|
|
|
if (sourceContent.GetType().Inherits(targetContentType) == false)
|
|
|
|
|
{
|
|
|
|
|
ThrowModelBindingException(true, true, sourceContent.GetType(), targetContentType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bindingContext.Result = ModelBindingResult.Success(Activator.CreateInstance(modelType, sourceContent));
|
2020-12-11 14:55:19 +11:00
|
|
|
return;
|
2020-05-08 13:23:44 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Last chance : try to convert
|
2020-12-11 14:55:19 +11:00
|
|
|
Attempt<object> attempt2 = source.TryConvertTo(modelType);
|
2020-05-08 13:23:44 +02:00
|
|
|
if (attempt2.Success)
|
|
|
|
|
{
|
|
|
|
|
bindingContext.Result = ModelBindingResult.Success(attempt2.Result);
|
2020-12-11 14:55:19 +11:00
|
|
|
return;
|
2020-05-08 13:23:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fail
|
|
|
|
|
ThrowModelBindingException(false, false, sourceType, modelType);
|
2020-12-11 14:55:19 +11:00
|
|
|
return;
|
2020-05-08 13:23:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ThrowModelBindingException(bool sourceContent, bool modelContent, Type sourceType, Type modelType)
|
|
|
|
|
{
|
|
|
|
|
var msg = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
// prepare message
|
|
|
|
|
msg.Append("Cannot bind source");
|
2020-12-10 18:09:32 +11:00
|
|
|
if (sourceContent)
|
2020-12-11 14:55:19 +11:00
|
|
|
{
|
2020-12-10 18:09:32 +11:00
|
|
|
msg.Append(" content");
|
2020-12-11 14:55:19 +11:00
|
|
|
}
|
|
|
|
|
|
2020-05-08 13:23:44 +02:00
|
|
|
msg.Append(" type ");
|
|
|
|
|
msg.Append(sourceType.FullName);
|
|
|
|
|
msg.Append(" to model");
|
2020-12-10 18:09:32 +11:00
|
|
|
if (modelContent)
|
2020-12-11 14:55:19 +11:00
|
|
|
{
|
2020-12-10 18:09:32 +11:00
|
|
|
msg.Append(" content");
|
2020-12-11 14:55:19 +11:00
|
|
|
}
|
|
|
|
|
|
2020-05-08 13:23:44 +02:00
|
|
|
msg.Append(" type ");
|
|
|
|
|
msg.Append(modelType.FullName);
|
|
|
|
|
msg.Append(".");
|
|
|
|
|
|
|
|
|
|
// raise event, to give model factories a chance at reporting
|
|
|
|
|
// the error with more details, and optionally request that
|
|
|
|
|
// the application restarts.
|
|
|
|
|
var args = new ModelBindingArgs(sourceType, modelType, msg);
|
|
|
|
|
ModelBindingException?.Invoke(this, args);
|
|
|
|
|
|
|
|
|
|
throw new ModelBindingException(msg.ToString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Contains event data for the <see cref="ModelBindingException"/> event.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class ModelBindingArgs : EventArgs
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Initializes a new instance of the <see cref="ModelBindingArgs"/> class.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public ModelBindingArgs(Type sourceType, Type modelType, StringBuilder message)
|
|
|
|
|
{
|
|
|
|
|
SourceType = sourceType;
|
|
|
|
|
ModelType = modelType;
|
|
|
|
|
Message = message;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the type of the source object.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Type SourceType { get; set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the type of the view model.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Type ModelType { get; set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the message string builder.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>Handlers of the event can append text to the message.</remarks>
|
|
|
|
|
public StringBuilder Message { get; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets a value indicating whether the application should restart.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool Restart { get; set; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Occurs on model binding exceptions.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static event EventHandler<ModelBindingArgs> ModelBindingException;
|
|
|
|
|
}
|
|
|
|
|
}
|