public AnimationBuilderStackEntry(Type type, int startIndex, int endIndex, int operationIndex) { Type = type; StartIndex = startIndex; EndIndex = endIndex; ObjectStartOffset = 0; Member = null; LeaveOperation = UpdateOperationType.Invalid; LeaveOffset = 0; OperationIndex = operationIndex; }
/// <summary> /// Registers a new member for a given type and name. /// </summary> /// <param name="owner">The owner type.</param> /// <param name="name">The member name.</param> /// <param name="updatableMember">The member update class to get and set value.</param> public static void RegisterMember(Type owner, string name, UpdatableMember updatableMember) { UpdateKeys[new UpdateKey(owner, name)] = updatableMember; }
private static void ProcessMember(ref ComputeUpdateOperationState state, UpdateMemberInfo animationPath, UpdatableMember updatableMember, List <object> temporaryObjectsList) { int leaveOffset = 0; var leaveOperation = UpdateOperationType.Invalid; // Note: only matters on leaf/leave nodes // It will be processed during following PopObjects state.LastChildMember = updatableMember; var updatableField = updatableMember as UpdatableField; if (updatableField != null) { // Apply field offset state.NewOffset += updatableField.Offset; if (state.ParseElementEnd == animationPath.Name.Length) { // Leaf node, perform the set operation state.UpdateOperations.Add(new UpdateOperation { Type = updatableField.GetSetOperationType(), Member = updatableField, AdjustOffset = state.NewOffset - state.PreviousOffset, DataOffset = animationPath.DataOffset, }); state.PreviousOffset = state.NewOffset; } else if (!updatableField.MemberType.GetTypeInfo().IsValueType) { // Only in case of objects we need to enter into them state.UpdateOperations.Add(new UpdateOperation { Type = UpdateOperationType.EnterObjectField, Member = updatableField, AdjustOffset = state.NewOffset - state.PreviousOffset, }); leaveOperation = UpdateOperationType.Leave; leaveOffset = state.NewOffset; state.PreviousOffset = state.NewOffset = 0; } } else { var updatableProperty = updatableMember as UpdatablePropertyBase; if (updatableProperty != null) { if (state.ParseElementEnd == animationPath.Name.Length) { // Leaf node, perform the set the value state.UpdateOperations.Add(new UpdateOperation { Type = updatableProperty.GetSetOperationType(), Member = updatableProperty, AdjustOffset = state.NewOffset - state.PreviousOffset, DataOffset = animationPath.DataOffset, }); state.PreviousOffset = state.NewOffset; } else { // Otherwise enter into the property bool isStruct = updatableProperty.MemberType.GetTypeInfo().IsValueType; int temporaryObjectIndex = -1; if (isStruct) { // Struct properties need a storage area so that we can later set the updated value back into the property leaveOperation = UpdateOperationType.LeaveAndCopyStructPropertyBase; temporaryObjectIndex = temporaryObjectsList.Count; temporaryObjectsList.Add(Activator.CreateInstance(updatableProperty.MemberType)); } else { leaveOperation = UpdateOperationType.Leave; } state.UpdateOperations.Add(new UpdateOperation { Type = updatableProperty.GetEnterOperationType(), Member = updatableProperty, AdjustOffset = state.NewOffset - state.PreviousOffset, DataOffset = temporaryObjectIndex, SkipCountIfNull = -1, }); leaveOffset = state.NewOffset; state.PreviousOffset = state.NewOffset = 0; } } } // No need to add the last part of the path, as we rarely set and then enter (and if we do we need to reevaluate updated value anyway) if (state.ParseElementEnd < animationPath.Name.Length) { state.StackPath.Add(new AnimationBuilderStackEntry(updatableMember.MemberType, state.ParseElementStart, state.ParseElementEnd, state.UpdateOperations.Count - 1) { Member = updatableMember, LeaveOperation = leaveOperation, LeaveOffset = leaveOffset, ObjectStartOffset = state.NewOffset, }); } }
/// <summary> /// Compiles a list of update operations into a <see cref="CompiledUpdate"/>, for use with <see cref="Run"/>. /// </summary> /// <param name="rootObjectType">The type of the root object.</param> /// <param name="animationPaths">The different paths and source offsets to use when <see cref="Run"/> is applied.</param> /// <returns>A <see cref="CompiledUpdate"/> object that can be used for <see cref="Run"/>.</returns> public static CompiledUpdate Compile(Type rootObjectType, List <UpdateMemberInfo> animationPaths) { var currentPath = string.Empty; var temporaryObjectsList = new List <object>(); var state = new ComputeUpdateOperationState(); state.UpdateOperations = new List <UpdateOperation>(); state.StackPath = new List <AnimationBuilderStackEntry> { new AnimationBuilderStackEntry(rootObjectType, 0, 0, -1), }; foreach (var animationPath in animationPaths) { var commonPathParts = 1; // Detect how much of the path is unmodified (note: we start from 1 because root always stay there) for (int index = 1; index < state.StackPath.Count; index++) { var pathPart = state.StackPath[index]; // Check if next path part is the same (first check length then content) if (((animationPath.Name.Length == pathPart.EndIndex) || (animationPath.Name.Length > pathPart.EndIndex && (animationPath.Name[pathPart.EndIndex] == PathDelimiter || animationPath.Name[pathPart.EndIndex] == PathIndexerOpen))) && string.Compare(animationPath.Name, pathPart.StartIndex, currentPath, pathPart.StartIndex, pathPart.EndIndex - pathPart.StartIndex, StringComparison.Ordinal) == 0) { commonPathParts++; continue; } break; } PopObjects(ref state, commonPathParts); // Parse the new path parts state.ParseElementStart = state.StackPath.Last().EndIndex; // Compute offset from start of current object state.NewOffset = state.StackPath.Last().ObjectStartOffset; while (state.ParseElementStart < animationPath.Name.Length) { var containerType = state.StackPath.Last().Type; // We have only two cases for now: array or property/field name bool isIndexerAccess = animationPath.Name[state.ParseElementStart] == PathIndexerOpen; if (isIndexerAccess) { // Parse until end of indexer state.ParseElementEnd = animationPath.Name.IndexOf(PathIndexerClose, state.ParseElementStart + 1); if (state.ParseElementEnd == -1) { throw new InvalidOperationException("Property path parse error: could not find indexer end ']'"); } // Include the indexer close state.ParseElementEnd++; // Parse integer // TODO: Avoid substring? var indexerString = animationPath.Name.Substring(state.ParseElementStart + 1, state.ParseElementEnd - state.ParseElementStart - 2); // T[], IList<T>, etc... // Try to resolve using custom resolver UpdatableMember updatableMember = null; var parentType = containerType; while (parentType != null) { UpdateMemberResolver resolver; if (MemberResolvers.TryGetValue(parentType, out resolver)) { updatableMember = resolver.ResolveIndexer(indexerString); if (updatableMember != null) { break; } } parentType = parentType.GetTypeInfo().BaseType; } // Try interfaces if (updatableMember == null) { foreach (var implementedInterface in containerType.GetTypeInfo().ImplementedInterfaces) { UpdateMemberResolver resolver; if (MemberResolvers.TryGetValue(implementedInterface, out resolver)) { updatableMember = resolver.ResolveIndexer(indexerString); if (updatableMember != null) { break; } } } } if (updatableMember == null) { throw new InvalidOperationException(string.Format("Property path parse error: could not find binding info for index {0} in type {1}", indexerString, containerType)); } ProcessMember(ref state, animationPath, updatableMember, temporaryObjectsList); } else { // Note: first character might be a . delimiter, if so, skip it var propertyStart = state.ParseElementStart; if (animationPath.Name[propertyStart] == PathDelimiter) { propertyStart++; } // Check if it started with a parenthesis (to perform a cast operation) if (animationPath.Name[propertyStart] == PathCastOpen) { // Parse until end of cast operation state.ParseElementEnd = animationPath.Name.IndexOf(PathCastClose, ++propertyStart); if (state.ParseElementEnd == -1) { throw new InvalidOperationException("Property path parse error: could not find cast operation ending ')'"); } var typeName = animationPath.Name.Substring(propertyStart, state.ParseElementEnd - propertyStart); // Include the indexer close state.ParseElementEnd++; // Try to resolve using alias first, then full assembly registry using assembly qualified name var type = DataSerializerFactory.GetTypeFromAlias(typeName) ?? AssemblyRegistry.GetType(typeName); if (type == null) { throw new InvalidOperationException($"Could not resolve type {typeName}"); } // Push entry with new type // TODO: Should we actually perform an early castclass and skip if type is incorrect? state.StackPath.Add(new AnimationBuilderStackEntry(type, state.ParseElementStart, state.ParseElementEnd, -1) { ObjectStartOffset = state.NewOffset, }); } else { UpdatableMember updatableMember = null; // Parse until end next group (or end) state.ParseElementEnd = animationPath.Name.IndexOfAny(PathGroupDelimiters, state.ParseElementStart + 1); if (state.ParseElementEnd == -1) { state.ParseElementEnd = animationPath.Name.Length; } var propertyName = animationPath.Name.Substring(propertyStart, state.ParseElementEnd - propertyStart); // TODO: Avoid substring? // try to find a member updater var parentType = containerType; while (parentType != null && !UpdateKeys.TryGetValue(new UpdateKey(parentType, propertyName), out updatableMember)) { parentType = parentType.GetTypeInfo().BaseType; } // if not found, try to find a custom resolver parentType = containerType; while (updatableMember == null && parentType != null) { UpdateMemberResolver resolver; if (MemberResolvers.TryGetValue(parentType, out resolver)) { updatableMember = resolver.ResolveProperty(propertyName); if (updatableMember != null) { break; } } parentType = parentType.GetTypeInfo().BaseType; } if (updatableMember == null) { throw new InvalidOperationException(string.Format("Property path parse error: could not find binding info for member {0} in type {1}", propertyName, containerType)); } // Process member ProcessMember(ref state, animationPath, updatableMember, temporaryObjectsList); } } state.ParseElementStart = state.ParseElementEnd; } currentPath = animationPath.Name; } // Totally pop the stack (we might still have stuff to copy back into properties PopObjects(ref state, 0); return(new CompiledUpdate { TemporaryObjects = temporaryObjectsList.ToArray(), UpdateOperations = state.UpdateOperations.ToArray(), }); }