This commit is contained in:
Oleksandr Liakhevych 2022-03-09 23:15:25 +02:00
Родитель bee609336a f95ed23311
Коммит 3d6a786489
8 изменённых файлов: 175 добавлений и 136 удалений

Просмотреть файл

@ -43,7 +43,7 @@ namespace Microsoft.MobileBlazorBindings.Core
/// <param name="parent"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public async Task<TComponent> AddComponent<TComponent>(IElementHandler parent, Dictionary<string, string> parameters = null) where TComponent : IComponent
public async Task<TComponent> AddComponent<TComponent>(IElementHandler parent, Dictionary<string, object> parameters = null) where TComponent : IComponent
{
return (TComponent)await AddComponent(typeof(TComponent), parent, parameters).ConfigureAwait(false);
}
@ -55,7 +55,7 @@ namespace Microsoft.MobileBlazorBindings.Core
/// <param name="parent"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public async Task<IComponent> AddComponent(Type componentType, IElementHandler parent, Dictionary<string, string> parameters = null)
public async Task<IComponent> AddComponent(Type componentType, IElementHandler parent, Dictionary<string, object> parameters = null)
{
try
{
@ -71,7 +71,7 @@ namespace Microsoft.MobileBlazorBindings.Core
_componentIdToAdapter[componentId] = rootAdapter;
SetNavigationParameters(component, parameters);
SetParameterArguments(component, parameters);
await RenderRootComponentAsync(componentId).ConfigureAwait(false);
return component;
@ -155,112 +155,35 @@ namespace Microsoft.MobileBlazorBindings.Core
return result;
}
public static void SetNavigationParameters(IComponent component, Dictionary<string, string> parameters)
internal static void SetParameterArguments(IComponent component, Dictionary<string, object> arguments)
{
if (component == null)
{
throw new ArgumentNullException(nameof(component));
}
if (parameters == null || parameters.Count == 0)
if (arguments == null || arguments.Count == 0)
{
//parameters will often be null. e.g. if you navigate with no parameters or when creating a root component.
return;
}
foreach (var parameter in parameters)
foreach (var parameter in arguments)
{
var prop = component.GetType().GetProperty(parameter.Key);
if (prop != null)
{
var parameterAttribute = prop.GetCustomAttribute(typeof(ParameterAttribute));
if (parameterAttribute == null)
{
throw new InvalidOperationException($"Object of type '{component.GetType()}' has a property matching the name '{parameter.Key}', but it does not have [ParameterAttribute] or [CascadingParameterAttribute] applied.");
}
if (TryParse(prop.PropertyType, parameter.Value, out var result))
{
prop.SetValue(component, result);
}
else
{
throw new InvalidOperationException($"Unable to set property {parameter.Key} on object of type '{component.GetType()}'.The value {parameter.Value}. can not be converted to a {prop.PropertyType.Name}");
}
}
else
if (prop == null)
{
throw new InvalidOperationException($"Object of type '{component.GetType()}' does not have a property matching the name '{parameter.Key}'.");
}
}
}
/// <summary>
/// Converts a string into the specified type. If conversion was successful, parsed property will be of the correct type and method will return true.
/// If conversion fails it will return false and parsed property will be null.
/// This method supports the 8 data types that are valid navigation parameters in Blazor. Passing a string is also safe but will be returned as is because no conversion is neccessary.
/// </summary>
/// <param name="type"></param>
/// <param name="s"></param>
/// <param name="result">The parsed object of the type specified. This will be null if conversion failed.</param>
/// <returns>True if s was converted successfully, otherwise false</returns>
public static bool TryParse(Type type, string s, out object result)
{
bool success;
var parameterAttribute = prop.GetCustomAttribute(typeof(ParameterAttribute));
if (parameterAttribute == null)
{
throw new InvalidOperationException($"Object of type '{component.GetType()}' has a property matching the name '{parameter.Key}', but it does not have [ParameterAttribute] or [CascadingParameterAttribute] applied.");
}
type = Nullable.GetUnderlyingType(type) ?? type;
if (type == typeof(string))
{
result = s;
success = true;
prop.SetValue(component, parameter.Value);
}
else if (type == typeof(int))
{
success = int.TryParse(s, out var parsed);
result = parsed;
}
else if (type == typeof(Guid))
{
success = Guid.TryParse(s, out var parsed);
result = parsed;
}
else if (type == typeof(bool))
{
success = bool.TryParse(s, out var parsed);
result = parsed;
}
else if (type == typeof(DateTime))
{
success = DateTime.TryParse(s, out var parsed);
result = parsed;
}
else if (type == typeof(decimal))
{
success = decimal.TryParse(s, out var parsed);
result = parsed;
}
else if (type == typeof(double))
{
success = double.TryParse(s, out var parsed);
result = parsed;
}
else if (type == typeof(float))
{
success = float.TryParse(s, out var parsed);
result = parsed;
}
else if (type == typeof(long))
{
success = long.TryParse(s, out var parsed);
result = parsed;
}
else
{
result = null;
success = false;
}
return success;
}
}
}

Просмотреть файл

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly:InternalsVisibleTo("Microsoft.MobileBlazorBindings.UnitTests, PublicKey=0024000004800000140100000602000000240000525341310008000001000100d569ddedcc845677b8d3a876b4fd0aee523a4260bfa2a62590184038fd15e6a78b4931a1501644ad1a087b2d3f949e407e52e98ec8fdfb49228b0e7abafb99aa83a5bb6021181a8a69e17e1b0ab4d9fdb1402b254cb56006c35fc46904ed83d1d795a0ceaf34600f3344718f8f81aa79a305fcd87acf01be47d29ddc4dc22db66bf2aea3102b1d51961acb0f3a8e66fcba8705c23f868cebfc7487f741dd3c249acf0bdbe5ad183cf2c5c20abaed017ca2e1b44f10504b90eee3245152ff2bd0198041645435ddaf4fb2cb5c1bc95c5915d7d338f1f20a38fe91892b5baa6b974630b9eb5ea508e2589d0bd8ea255b9b0869ed1b843521c9fc511d482a81c7df")]

Просмотреть файл

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<SignAssembly>true</SignAssembly>
</PropertyGroup>
<ItemGroup>

Просмотреть файл

@ -13,36 +13,6 @@ namespace Microsoft.MobileBlazorBindings.UnitTests
[TestFixture]
public class NativeComponentRendererTests
{
[SetUp]
public void Setup()
{
}
private static Guid testGuid = Guid.NewGuid();
public static IEnumerable<TestCaseData> TryParseTestData
{
get
{
yield return new TestCaseData("s", typeof(string), "s", true).SetName("Parse valid string");
yield return new TestCaseData("5", typeof(int), 5, true).SetName("Parse valid int");
yield return new TestCaseData("invalid text", typeof(int), 0, false).SetName("Parse invalid int");
yield return new TestCaseData("2020-05-20", typeof(DateTime), new DateTime(2020, 05, 20), true).SetName("Parse valid date");
yield return new TestCaseData("invalid text", typeof(DateTime), new DateTime(), false).SetName("Parse invalid date");
yield return new TestCaseData(testGuid.ToString(), typeof(Guid), testGuid, true).SetName("Parse valid GUID");
yield return new TestCaseData("invalid text", typeof(Guid), new Guid(), false).SetName("Parse invalid GUID");
yield return new TestCaseData("{'value': '5'}", typeof(object), null, false).SetName("Parse POCO should find null operation");
}
}
[TestCaseSource(typeof(NativeComponentRendererTests), nameof(TryParseTestData))]
public void TryParseTest(string s, Type type, object expectedResult, bool expectedSuccess)
{
var success = NativeComponentRenderer.TryParse(type, s, out var result);
Assert.Multiple(() =>
{
Assert.AreEqual(expectedResult, result);
Assert.AreEqual(expectedSuccess, success);
});
}
#pragma warning disable CA1034 // Nested types should not be visible; this is test-only code
public class TestComponent : ComponentBase
@ -50,6 +20,8 @@ namespace Microsoft.MobileBlazorBindings.UnitTests
{
[Parameter] public string StringParameter { get; set; }
[Parameter] public int IntParameter { get; set; }
[Parameter] public int? NullableIntParameter { get; set; }
[Parameter] public object ObjectParameter { get; set; }
public string NonParameter { get; set; }
}
@ -57,40 +29,45 @@ namespace Microsoft.MobileBlazorBindings.UnitTests
{
get
{
yield return new TestCaseData(new Dictionary<string, string> { { "StringParameter", "paravalue" } }, "paravalue").SetName("Set string parameter");
yield return new TestCaseData(new Dictionary<string, string> { { "IntParameter", "5" } }, 5).SetName("Set int parameter");
yield return new TestCaseData(new Dictionary<string, object> { { "StringParameter", "paravalue" } }).SetName("Set string parameter");
yield return new TestCaseData(new Dictionary<string, object> { { "StringParameter", null } }).SetName("Set string parameter to null");
yield return new TestCaseData(new Dictionary<string, object> { { "IntParameter", 5 } }).SetName("Set int parameter");
yield return new TestCaseData(new Dictionary<string, object> { { "NullableIntParameter", 5 } }).SetName("Set int? parameter");
yield return new TestCaseData(new Dictionary<string, object> { { "NullableIntParameter", null } }).SetName("Set int? parameter to null");
yield return new TestCaseData(new Dictionary<string, object> { { "ObjectParameter", "stringObject" } }).SetName("Set object parameter");
}
}
[TestCaseSource(typeof(NativeComponentRendererTests), nameof(SetParameterTestData))]
public void SetParameterTest(Dictionary<string, string> parameters, object expected)
[TestCaseSource(nameof(SetParameterTestData))]
public void SetParameterTest(Dictionary<string, object> parameters)
{
var component = new TestComponent();
NativeComponentRenderer.SetNavigationParameters(component, parameters);
NativeComponentRenderer.SetParameterArguments(component, parameters);
var prop = component.GetType().GetProperty(parameters.FirstOrDefault().Key);
var value = prop.GetValue(component);
Assert.AreEqual(expected, value);
var parameterKeyValue = parameters.FirstOrDefault();
var prop = component.GetType().GetProperty(parameterKeyValue.Key);
var actualValue = prop.GetValue(component);
Assert.AreEqual(parameterKeyValue.Value, actualValue);
}
[Test]
public void SetIntToString()
{
var component = new TestComponent();
var expected = "NotAnInt";
var value = "NotAnInt";
var parameters = new Dictionary<string, string> { { "IntParameter", expected } };
Assert.Throws<InvalidOperationException>(() => NativeComponentRenderer.SetNavigationParameters(component, parameters));
var parameters = new Dictionary<string, object> { { "IntParameter", value } };
Assert.Throws<ArgumentException>(() => NativeComponentRenderer.SetParameterArguments(component, parameters));
}
[Test]
public void SetNonParameter()
{
var component = new TestComponent();
var expected = "NonParameter";
var value = "NonParameter";
var parameters = new Dictionary<string, string> { { "NonParameter", expected } };
Assert.Throws<InvalidOperationException>(() => NativeComponentRenderer.SetNavigationParameters(component, parameters));
var parameters = new Dictionary<string, object> { { "NonParameter", value } };
Assert.Throws<InvalidOperationException>(() => NativeComponentRenderer.SetParameterArguments(component, parameters));
}
}
}

Просмотреть файл

@ -0,0 +1,40 @@
using Microsoft.MobileBlazorBindings.Core;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.MobileBlazorBindings.UnitTests.ShellNavigation
{
public class ShellNavigationManagerTests
{
private static Guid testGuid = Guid.NewGuid();
public static IEnumerable<TestCaseData> TryParseTestData
{
get
{
yield return new TestCaseData("s", typeof(string), "s", true).SetName("Parse valid string");
yield return new TestCaseData("5", typeof(int), 5, true).SetName("Parse valid int");
yield return new TestCaseData("5", typeof(int?), 5, true).SetName("Parse valid int?");
yield return new TestCaseData("invalid text", typeof(int), 0, false).SetName("Parse invalid int");
yield return new TestCaseData("2020-05-20", typeof(DateTime), new DateTime(2020, 05, 20), true).SetName("Parse valid date");
yield return new TestCaseData("invalid text", typeof(DateTime), new DateTime(), false).SetName("Parse invalid date");
yield return new TestCaseData(testGuid.ToString(), typeof(Guid), testGuid, true).SetName("Parse valid GUID");
yield return new TestCaseData(testGuid.ToString(), typeof(Guid?), testGuid, true).SetName("Parse valid GUID?");
yield return new TestCaseData("invalid text", typeof(Guid), new Guid(), false).SetName("Parse invalid GUID");
yield return new TestCaseData("{'value': '5'}", typeof(object), null, false).SetName("Parse POCO should find null operation");
}
}
[TestCaseSource(nameof(TryParseTestData))]
public void TryParseTest(string s, Type type, object expectedResult, bool expectedSuccess)
{
var success = ShellNavigationManager.TryParse(type, s, out var result);
Assert.Multiple(() =>
{
Assert.AreEqual(expectedResult, result);
Assert.AreEqual(expectedSuccess, success);
});
}
}
}

Просмотреть файл

@ -21,7 +21,7 @@ namespace Microsoft.MobileBlazorBindings
public override Dispatcher Dispatcher { get; } = new XamarinDeviceDispatcher();
public Task<TComponent> AddComponent<TComponent>(MC.Element parent, Dictionary<string, string> parameters = null) where TComponent : IComponent
public Task<TComponent> AddComponent<TComponent>(MC.Element parent, Dictionary<string, object> parameters = null) where TComponent : IComponent
{
if (parent is MC.Application app)
{

Просмотреть файл

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly:InternalsVisibleTo("Microsoft.MobileBlazorBindings.UnitTests, PublicKey=0024000004800000140100000602000000240000525341310008000001000100d569ddedcc845677b8d3a876b4fd0aee523a4260bfa2a62590184038fd15e6a78b4931a1501644ad1a087b2d3f949e407e52e98ec8fdfb49228b0e7abafb99aa83a5bb6021181a8a69e17e1b0ab4d9fdb1402b254cb56006c35fc46904ed83d1d795a0ceaf34600f3344718f8f81aa79a305fcd87acf01be47d29ddc4dc22db66bf2aea3102b1d51961acb0f3a8e66fcba8705c23f868cebfc7487f741dd3c249acf0bdbe5ad183cf2c5c20abaed017ca2e1b44f10504b90eee3245152ff2bd0198041645435ddaf4fb2cb5c1bc95c5915d7d338f1f20a38fe91892b5baa6b974630b9eb5ea508e2589d0bd8ea255b9b0869ed1b843521c9fc511d482a81c7df")]

Просмотреть файл

@ -108,7 +108,8 @@ namespace Microsoft.MobileBlazorBindings
var renderer = new MobileBlazorBindingsRenderer(_services, _services.GetRequiredService<ILoggerFactory>());
#pragma warning restore CA2000 // Dispose objects before losing scope
var addComponentTask = renderer.AddComponent(componentType, container, route.Parameters);
var convertedParameters = ConvertParameters(componentType, route.Parameters);
var addComponentTask = renderer.AddComponent(componentType, container, convertedParameters);
var elementAddedTask = container.WaitForElementAsync();
await Task.WhenAny(addComponentTask, elementAddedTask).ConfigureAwait(false);
@ -147,5 +148,96 @@ namespace Microsoft.MobileBlazorBindings
}
}
}
internal static Dictionary<string, object> ConvertParameters(Type componentType, Dictionary<string, string> parameters)
{
if (parameters is null)
{
return null;
}
var convertedParameters = new Dictionary<string, object>();
foreach (var keyValue in parameters)
{
var propertyType = componentType.GetProperty(keyValue.Key)?.PropertyType ?? typeof(string);
if (!TryParse(propertyType, keyValue.Value, out var parsedValue))
{
throw new InvalidOperationException($"The value {keyValue.Value} can not be converted to a {propertyType.Name}");
}
convertedParameters[keyValue.Key] = parsedValue;
}
return convertedParameters;
}
/// <summary>
/// Converts a string into the specified type. If conversion was successful, parsed property will be of the correct type and method will return true.
/// If conversion fails it will return false and parsed property will be null.
/// This method supports the 8 data types that are valid navigation parameters in Blazor. Passing a string is also safe but will be returned as is because no conversion is neccessary.
/// </summary>
/// <param name="type"></param>
/// <param name="s"></param>
/// <param name="result">The parsed object of the type specified. This will be null if conversion failed.</param>
/// <returns>True if s was converted successfully, otherwise false</returns>
internal static bool TryParse(Type type, string s, out object result)
{
bool success;
type = Nullable.GetUnderlyingType(type) ?? type;
if (type == typeof(string))
{
result = s;
success = true;
}
else if (type == typeof(int))
{
success = int.TryParse(s, out var parsed);
result = parsed;
}
else if (type == typeof(Guid))
{
success = Guid.TryParse(s, out var parsed);
result = parsed;
}
else if (type == typeof(bool))
{
success = bool.TryParse(s, out var parsed);
result = parsed;
}
else if (type == typeof(DateTime))
{
success = DateTime.TryParse(s, out var parsed);
result = parsed;
}
else if (type == typeof(decimal))
{
success = decimal.TryParse(s, out var parsed);
result = parsed;
}
else if (type == typeof(double))
{
success = double.TryParse(s, out var parsed);
result = parsed;
}
else if (type == typeof(float))
{
success = float.TryParse(s, out var parsed);
result = parsed;
}
else if (type == typeof(long))
{
success = long.TryParse(s, out var parsed);
result = parsed;
}
else
{
result = null;
success = false;
}
return success;
}
}
}