Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

Allow users to specify resolution method for handlers, effects, and services #1870

Merged
merged 7 commits into from
Feb 26, 2018
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions Xamarin.Forms.Core.UnitTests/DependencyResolutionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;

namespace Xamarin.Forms.Core.UnitTests
{
[TestFixture]
public class DependencyResolutionTests : BaseTestFixture
{
class MockElement { }

class MockElementRenderer : IRegisterable { }

class MockRendererWithParam : IRegisterable
{
readonly object _aParameter;

public MockRendererWithParam(object aParameter)
{
_aParameter = aParameter ?? throw new ArgumentNullException(nameof(aParameter));
}
}

class MockEffect : Effect
{
protected override void OnAttached()
{
throw new NotImplementedException();
}

protected override void OnDetached()
{
throw new NotImplementedException();
}
}

interface IMockService { }

class MockServiceImpl : IMockService { }

// ReSharper disable once ClassNeverInstantiated.Local
// (Just testing the type registration, don't need an instance)
class MockServiceImpl2 : IMockService { }

class MockContainer
{
readonly Dictionary<Type, object> _services;
readonly Dictionary<Type, Func<object, object>> _factories;

public MockContainer()
{
_services = new Dictionary<Type, object>();
_factories = new Dictionary<Type, Func<object, object>>();
}

public void Register(Type type, object result)
{
_services.Add(type, result);
}

public void Register(Type type, Func<object, object> factory)
{
_factories.Add(type, factory);
}

public object Resolve(Type type, params object[] args)
{
if (_services.ContainsKey(type))
{
return _services[type];
}

if (_factories.ContainsKey(type))
{
return _factories[type].Invoke(args[0]);
}

return null;
}
}

MockContainer _container;

[SetUp]
public void SetUp()
{
_container = new MockContainer();

object ResolveDelegate(Type type, object[] args) => _container.Resolve(type, args);

Internals.DependencyResolver.ResolveUsing(ResolveDelegate);
}

[Test]
public void GetHandlerFromContainer()
{
Internals.Registrar.Registered.Register(typeof(MockElement), typeof(MockElementRenderer));
var renderer = new MockElementRenderer();
_container.Register(typeof(MockElementRenderer), renderer);
var result = Internals.Registrar.Registered.GetHandler(typeof(MockElement));
var typedRenderer = (MockElementRenderer)result;

Assert.That(typedRenderer, Is.SameAs(renderer));
}

[Test]
public void GetEffectFromContainer()
{
string effectName = "anEffect";
Internals.Registrar.Effects[effectName] = typeof(MockEffect);
var effect = new MockEffect();
_container.Register(typeof(MockEffect), effect);
var result = Effect.Resolve(effectName);

Assert.That(result, Is.SameAs(effect));
}

[Test]
public void GetServiceFromContainer()
{
MockServiceImpl impl = new MockServiceImpl();
_container.Register(typeof(IMockService), impl);
DependencyService.Register<MockServiceImpl>();
var result = DependencyService.Resolve<IMockService>();

Assert.That(result, Is.SameAs(impl));
}

[Test]
public void PreferServiceTypeFromContainer()
{
MockServiceImpl impl = new MockServiceImpl();
_container.Register(typeof(IMockService), impl);
DependencyService.Register<IMockService, MockServiceImpl2>();
var result = DependencyService.Resolve<IMockService>();

Assert.That(result, Is.SameAs(impl));
}

[Test]
public void FallbackOnDependencyServiceIfNotInContainer()
{
DependencyService.Register<MockServiceImpl>();
var result = DependencyService.Resolve<IMockService>();

Assert.That(result, Is.Not.Null);
}

[Test]
public void HandlerWithParameter()
{
Internals.Registrar.Registered.Register(typeof(MockElement), typeof(MockRendererWithParam));

_container.Register(typeof(MockRendererWithParam), (o) => new MockRendererWithParam(o));

var context = "Pretend this is an Android context";

var result = Internals.Registrar.Registered.GetHandler(typeof(MockElement), context);
var typedRenderer = (MockRendererWithParam)result;

Assert.That(typedRenderer, Is.InstanceOf(typeof(MockRendererWithParam)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
<Compile Include="ColorUnitTests.cs" />
<Compile Include="CommandSourceTests.cs" />
<Compile Include="CommandTests.cs" />
<Compile Include="DependencyResolutionTests.cs" />
<Compile Include="EffectiveFlowDirectionExtensions.cs" />
<Compile Include="FlowDirectionTests.cs" />
<Compile Include="LayoutChildIntoBoundingRegionTests.cs" />
Expand Down Expand Up @@ -225,7 +226,5 @@
<LogicalName>Images/crimson.jpg</LogicalName>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Folder Include="StyleSheets\" />
</ItemGroup>
</Project>
<ItemGroup />
</Project>
49 changes: 49 additions & 0 deletions Xamarin.Forms.Core/DependencyResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Linq;
using System.Reflection;

namespace Xamarin.Forms.Internals
{
public static class DependencyResolver
{
public delegate object ResolveDelegate(Type type, params object[] args);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest we use Func<Type, object[], object>; Suggest we conserve the number of new Types introduced at the expense syntactic sugar at our call site; We can just put the args into a new object[] and save ourselves a bit of public surface area, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


static ResolveDelegate Resolver { get; set; }

public static void ResolveUsing(ResolveDelegate resolveDelegate)
{
Resolver = resolveDelegate;
}

public static void ResolveUsing(Func<Type, object> resolveFunc)
{
object ResolveDelegate(Type type, object[] args) => resolveFunc.Invoke(type);
Resolver = ResolveDelegate;
}

internal static object Resolve(Type type, params object[] args)
{
return Resolver?.Invoke(type, args);
}

internal static object ForceResolve(Type type, params object[] args)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest ResolveOrActivate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with ResolveOrCreate.

{
var result = Resolve(type, args);

if (result != null) return result;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest we throw if Type is not assignable from the result.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


if (args.Length > 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what case the following code handles; Suggest we add unit tests to cover 35:46.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
// This is by no means a general solution to matching with the correct constructor, but it'll
// do for finding Android renderers which need Context (vs older custom renderers which may still use
// parameterless constructors)
if (type.GetTypeInfo().DeclaredConstructors.Any(info => info.GetParameters().Length == args.Length))
{
return Activator.CreateInstance(type, args);
}
}

return Activator.CreateInstance(type);
}
}
}
7 changes: 7 additions & 0 deletions Xamarin.Forms.Core/DependencyService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ public static class DependencyService
static readonly List<Type> DependencyTypes = new List<Type>();
static readonly Dictionary<Type, DependencyData> DependencyImplementations = new Dictionary<Type, DependencyData>();

public static T Resolve<T>(DependencyFetchTarget fallbackFetchTarget = DependencyFetchTarget.GlobalInstance) where T : class
{
var result = DependencyResolver.Resolve(typeof(T)) as T;

return result ?? Get<T>(fallbackFetchTarget);
}

public static T Get<T>(DependencyFetchTarget fetchTarget = DependencyFetchTarget.GlobalInstance) where T : class
{
Initialize();
Expand Down
5 changes: 2 additions & 3 deletions Xamarin.Forms.Core/Effect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@ internal Effect()

public static Effect Resolve(string name)
{
Type effectType;
Effect result = null;
if (Internals.Registrar.Effects.TryGetValue(name, out effectType))
if (Internals.Registrar.Effects.TryGetValue(name, out Type effectType))
{
result = (Effect)Activator.CreateInstance(effectType);
result = DependencyResolver.ForceResolve(effectType) as Effect;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest considering cast instead of as; The result should either be of effectType or null, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. Done.

}

if (result == null)
Expand Down
17 changes: 5 additions & 12 deletions Xamarin.Forms.Core/Registrar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ internal static class Registrar
internal static void RegisterAll(Type[] attrTypes) => Internals.Registrar.RegisterAll(attrTypes);
}
}

namespace Xamarin.Forms.Internals
{
[EditorBrowsable(EditorBrowsableState.Never)]
Expand All @@ -33,7 +34,8 @@ internal TRegistrable GetHandler(Type type)
if (handlerType == null)
return null;

object handler = Activator.CreateInstance(handlerType);
object handler = DependencyResolver.ForceResolve(handlerType);

return (TRegistrable)handler;
}

Expand All @@ -48,16 +50,7 @@ internal TRegistrable GetHandler(Type type, params object[] args)
if (handlerType == null)
return null;

// This is by no means a general solution to matching with the correct constructor, but it'll
// do for finding Android renderers which need Context (vs older custom renderers which may still use
// parameterless constructors)
if (handlerType.GetTypeInfo().DeclaredConstructors.Any(info => info.GetParameters().Length == args.Length))
{
object handler = Activator.CreateInstance(handlerType, args);
return (TRegistrable)handler;
}

return GetHandler(type);
return (TRegistrable)DependencyResolver.ForceResolve(handlerType, args);
}

public TOut GetHandler<TOut>(Type type) where TOut : TRegistrable
Expand Down Expand Up @@ -171,7 +164,7 @@ static Registrar()

public static IEnumerable<Assembly> ExtraAssemblies { get; set; }

public static Registrar<IRegisterable> Registered { get; }
public static Registrar<IRegisterable> Registered { get; internal set; }

public static void RegisterAll(Type[] attrTypes)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Type Name="DependencyResolver+ResolveDelegate" FullName="Xamarin.Forms.Internals.DependencyResolver+ResolveDelegate">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might consider if some dev time now drafting some isense docs might preempt questions/confusion in the forums.

<TypeSignature Language="C#" Value="public delegate object DependencyResolver.ResolveDelegate(Type type, object[] args);" />
<TypeSignature Language="ILAsm" Value=".class nested public auto ansi sealed DependencyResolver/ResolveDelegate extends System.MulticastDelegate" />
<AssemblyInfo>
<AssemblyName>Xamarin.Forms.Core</AssemblyName>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
</AssemblyInfo>
<Base>
<BaseTypeName>System.Delegate</BaseTypeName>
</Base>
<Parameters>
<Parameter Name="type" Type="System.Type" />
<Parameter Name="args" Type="System.Object[]">
<Attributes>
<Attribute>
<AttributeName>System.ParamArray</AttributeName>
</Attribute>
</Attributes>
</Parameter>
</Parameters>
<ReturnValue>
<ReturnType>System.Object</ReturnType>
</ReturnValue>
<Docs>
<param name="type">To be added.</param>
<param name="args">To be added.</param>
<summary>To be added.</summary>
<returns>To be added.</returns>
<remarks>To be added.</remarks>
</Docs>
</Type>
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<Type Name="DependencyResolver" FullName="Xamarin.Forms.Internals.DependencyResolver">
<TypeSignature Language="C#" Value="public static class DependencyResolver" />
<TypeSignature Language="ILAsm" Value=".class public auto ansi abstract sealed beforefieldinit DependencyResolver extends System.Object" />
<AssemblyInfo>
<AssemblyName>Xamarin.Forms.Core</AssemblyName>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
</AssemblyInfo>
<Base>
<BaseTypeName>System.Object</BaseTypeName>
</Base>
<Interfaces />
<Docs>
<summary>To be added.</summary>
<remarks>To be added.</remarks>
</Docs>
<Members>
<Member MemberName="ResolveUsing">
<MemberSignature Language="C#" Value="public static void ResolveUsing (Func&lt;Type,object&gt; resolveFunc);" />
<MemberSignature Language="ILAsm" Value=".method public static hidebysig void ResolveUsing(class System.Func`2&lt;class System.Type, object&gt; resolveFunc) cil managed" />
<MemberType>Method</MemberType>
<AssemblyInfo>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
</AssemblyInfo>
<ReturnValue>
<ReturnType>System.Void</ReturnType>
</ReturnValue>
<Parameters>
<Parameter Name="resolveFunc" Type="System.Func&lt;System.Type,System.Object&gt;" />
</Parameters>
<Docs>
<param name="resolveFunc">To be added.</param>
<summary>To be added.</summary>
<remarks>To be added.</remarks>
</Docs>
</Member>
<Member MemberName="ResolveUsing">
<MemberSignature Language="C#" Value="public static void ResolveUsing (Xamarin.Forms.Internals.DependencyResolver.ResolveDelegate resolveDelegate);" />
<MemberSignature Language="ILAsm" Value=".method public static hidebysig void ResolveUsing(class Xamarin.Forms.Internals.DependencyResolver/ResolveDelegate resolveDelegate) cil managed" />
<MemberType>Method</MemberType>
<AssemblyInfo>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
</AssemblyInfo>
<ReturnValue>
<ReturnType>System.Void</ReturnType>
</ReturnValue>
<Parameters>
<Parameter Name="resolveDelegate" Type="Xamarin.Forms.Internals.DependencyResolver+ResolveDelegate" />
</Parameters>
<Docs>
<param name="resolveDelegate">To be added.</param>
<summary>To be added.</summary>
<remarks>To be added.</remarks>
</Docs>
</Member>
</Members>
</Type>
Loading