Skip to content

Instantly share code, notes, and snippets.

@Xceno
Last active April 24, 2017 14:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Xceno/f79a3def10b4a5100538ef9fa34138a5 to your computer and use it in GitHub Desktop.
Save Xceno/f79a3def10b4a5100538ef9fa34138a5 to your computer and use it in GitHub Desktop.
An override for Orchards DefaultShapeFactory, implementing changes discussed in #7073
namespace Orchard.Tools
{
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using Orchard.DisplayManagement;
using Orchard.DisplayManagement.Descriptors;
using Orchard.DisplayManagement.Implementation;
using Orchard.DisplayManagement.Shapes;
using Orchard.Environment.Extensions;
/// <summary>
/// An override for Orchards DefaultShapeFactory, implementing changes discussed in #7073, to allow better support for strongly typed viewModels.<br/>
/// See those discussions:<br/>
/// https://github.com/OrchardCMS/Orchard/issues/5514<br/>
/// https://github.com/OrchardCMS/Orchard/issues/7073
/// </summary>
[OrchardSuppressDependency("Orchard.DisplayManagement.Implementation.DefaultShapeFactory")]
public class ViewModelFriendlyShapeFactoy : Composite, IShapeFactory
{
private readonly IEnumerable<Lazy<IShapeFactoryEvents>> events;
private readonly Lazy<IShapeTableLocator> shapeTableLocator;
public ViewModelFriendlyShapeFactoy(
IEnumerable<Lazy<IShapeFactoryEvents>> events,
Lazy<IShapeTableLocator> shapeTableLocator)
{
this.events = events;
this.shapeTableLocator = shapeTableLocator;
}
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
result = this.Create(binder.Name, Arguments.From(args, binder.CallInfo.ArgumentNames));
return true;
}
public IShape Create(string shapeType)
{
return this.Create(shapeType, Arguments.Empty(), () => new Shape());
}
public IShape Create(string shapeType, INamedEnumerable<object> parameters)
{
return this.Create(shapeType, parameters, () => new Shape());
}
public IShape Create(string shapeType, INamedEnumerable<object> parameters, Func<dynamic> createShape)
{
var defaultShapeTable = this.shapeTableLocator.Value.Lookup(null);
ShapeDescriptor shapeDescriptor;
defaultShapeTable.Descriptors.TryGetValue(shapeType, out shapeDescriptor);
parameters = parameters ?? Arguments.Empty();
var creatingContext = new ShapeCreatingContext
{
New = this,
ShapeFactory = this,
ShapeType = shapeType,
OnCreated = new List<Action<ShapeCreatedContext>>()
};
IEnumerable<object> positional = parameters.Positional.ToList();
var baseType = positional.FirstOrDefault() as Type;
if ( baseType == null )
{
// default to common base class
creatingContext.Create = createShape ?? ( () => new Shape() );
}
else
{
// consume the first argument
positional = positional.Skip(1);
var initializerObject = positional.SingleOrDefault();
// We need to make sure that the initializerShape is of the same type as the specified baseType.
// This should prevent any issues when someone passes in another type as the second, unnamed parameter like this:
// shapeFactory.Parts_MyPart(typeof(MyPart), new UnrelatedViewModelWithExtraProps())
// If this scenario occurs, we skip the whole init process and proceed as normal.
var initializerHasCorrectType = initializerObject != null && initializerObject.GetType() == baseType;
dynamic initializerShape = initializerObject as IShape; // The cast preserves the Metadata property.
if ( initializerHasCorrectType && initializerShape != null )
{
// note:
// we need to clear alternates here because when using the same initializer shape twice like ...
//
// @Display.MyShapeHeaderBefore(Model.Header.GetType(), Model.Header) *1
// @Display.MyShapeHeader(Model.Header.GetType(), Model.Header) *2
//
// ... it might be possible that *1 results in an alternate being added to the initializer
// shape (e.g. via IShapeTableProvider) whereas *2 would then render the alternate instead of the normal shape
ShapeMetadata initShapeMetadata = initializerShape.Metadata;
if ( initShapeMetadata != null )
initShapeMetadata.Alternates.Clear();
creatingContext.Create = () => initializerShape; // return initializer shape directly on create call instead of creating a new instance ...
positional = positional.Skip(1); // ... and thus we are able to skip updating the copy (which is very costly on complex shape scenarios), see *3
}
else
{
creatingContext.Create = () => Activator.CreateInstance(baseType);
}
}
// "creating" events may add behaviors and alter base type)
foreach ( var ev in this.events )
{
ev.Value.Creating(creatingContext);
}
if ( shapeDescriptor != null )
{
foreach ( var ev in shapeDescriptor.Creating )
{
ev(creatingContext);
}
}
// create the new instance
var createdContext = new ShapeCreatedContext
{
New = creatingContext.New,
ShapeType = creatingContext.ShapeType,
Shape = creatingContext.Create()
};
if ( !( createdContext.Shape is IShape ) )
{
throw new InvalidOperationException("Invalid base type for shape: " + createdContext.Shape.GetType().ToString());
}
ShapeMetadata shapeMetadata = createdContext.Shape.Metadata;
createdContext.Shape.Metadata.Type = shapeType;
if ( shapeDescriptor != null )
shapeMetadata.Wrappers = shapeMetadata.Wrappers.Concat(shapeDescriptor.Wrappers).ToList();
// "created" events provides default values and new object initialization
foreach ( var ev in this.events )
{
ev.Value.Created(createdContext);
}
if ( shapeDescriptor != null )
{
foreach ( var ev in shapeDescriptor.Created )
{
ev(createdContext);
}
}
foreach ( var ev in creatingContext.OnCreated )
{
ev(createdContext);
}
// other properties passed with call overlay any defaults, so are after the created events
// only one non-Type, non-named argument is remaining
var initializer = positional.SingleOrDefault();
if ( initializer != null )
{
foreach ( var prop in initializer.GetType().GetProperties() )
{
createdContext.Shape[prop.Name] = prop.GetValue(initializer, null);
}
}
foreach ( var kv in parameters.Named )
{
createdContext.Shape[kv.Key] = kv.Value;
}
return createdContext.Shape;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment