using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; namespace Artemis.UI.Shared.Routing; /// /// Represents a view model to which routing with parameters can take place. /// /// The type of parameters the screen expects. It must have a parameterless constructor. public abstract class RoutableScreen : RoutableScreen, IRoutableScreen where TParam : new() { /// /// Gets or sets the parameter source of the screen. /// protected ParameterSource ParameterSource { get; set; } = ParameterSource.Segment; /// /// Called while navigating to this screen. /// /// An object containing the parameters of the navigation action. /// Navigation arguments containing information about the navigation action. /// /// A cancellation token that can be used by other objects or threads to receive notice of /// cancellation. /// public virtual Task OnNavigating(TParam parameters, NavigationArguments args, CancellationToken cancellationToken) { return Task.CompletedTask; } async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken) { Func activator = GetParameterActivator(); object[] routeParameters = ParameterSource == ParameterSource.Segment ? args.SegmentParameters : args.RouteParameters; if (routeParameters.Length != _parameterPropertyCount) throw new ArtemisRoutingException($"Did not retrieve the required amount of parameters, expects {_parameterPropertyCount}, got {routeParameters.Length}."); TParam parameters = activator(routeParameters); await OnNavigating(args, cancellationToken); await OnNavigating(parameters, args, cancellationToken); } async Task IRoutableScreen.InternalOnClosing(NavigationArguments args) { await OnClosing(args); } #region Parameter generation // ReSharper disable once StaticMemberInGenericType - That's intentional, each kind of TParam should have its own property count private static int _parameterPropertyCount; private static Func? _parameterActivator; private static Func GetParameterActivator() { if (_parameterActivator != null) return _parameterActivator; // Generates a lambda that creates a new instance of TParam // - Each property of TParam with a public setter must be set using the source object[] // - Use the index of each property as the index on the source array // - Cast the object of the source array to the correct type for that property Type parameterType = typeof(TParam); ParameterExpression sourceExpression = Expression.Parameter(typeof(object[]), "source"); ParameterExpression parameterExpression = Expression.Parameter(parameterType, "parameters"); List propertyAssignments = parameterType.GetProperties() .Where(p => p.CanWrite) .Select((property, index) => { UnaryExpression sourceValueExpression = Expression.Convert( Expression.ArrayIndex(sourceExpression, Expression.Constant(index)), property.PropertyType ); BinaryExpression propertyAssignment = Expression.Assign( Expression.Property(parameterExpression, property), sourceValueExpression ); return propertyAssignment; }) .ToList(); Expression> lambda = Expression.Lambda>( Expression.Block( new[] {parameterExpression}, Expression.Assign(parameterExpression, Expression.New(parameterType)), Expression.Block(propertyAssignments), parameterExpression ), sourceExpression ); _parameterActivator = lambda.Compile(); _parameterPropertyCount = propertyAssignments.Count; return _parameterActivator; } #endregion } /// /// Enum representing the source of parameters in the RoutableScreen class. /// public enum ParameterSource { /// /// Represents the source where parameters are obtained from the segment of the route. /// Segment, /// /// Represents the source where parameters are obtained from the entire route. /// Route }