//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ namespace System.Activities.Statements { using System; using System.Activities; using System.Activities.DynamicUpdate; using System.Activities.Expressions; using System.Activities.Validation; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime; using System.Runtime.Collections; using System.Windows.Markup; /// /// This class represents a StateMachine which contains States and Variables. /// [ContentProperty("States")] public sealed class StateMachine : NativeActivity { // internal Id of StateMachine. it's a constant value and states of state machine will generate their ids based on this root id. const string RootId = "0"; const string ExitProperty = "Exit"; static Func getDefaultExtension = new Func(GetStateMachineExtension); // states in root level of StateMachine Collection states; // variables used in StateMachine Collection variables; // internal representations of states Collection internalStates; // ActivityFuncs who call internal activities Collection> internalStateFuncs; // Callback when a state completes CompletionCallback onStateComplete; // eventManager is used to manage the events of trigger completion. // When a trigger on a transition is completed, the corresponding event will be sent to eventManager. // eventManager will decide whether immediate process it or just register it. Variable eventManager; /// /// It's constructor. /// public StateMachine() { this.internalStates = new Collection(); this.internalStateFuncs = new Collection>(); this.eventManager = new Variable { Name = "EventManager", Default = new StateMachineEventManagerFactory() }; this.onStateComplete = new CompletionCallback(this.OnStateComplete); } /// /// Gets or sets the start point of the StateMachine. /// [DefaultValue(null)] public State InitialState { get; set; } /// /// Gets all root level States in the StateMachine. /// [DependsOn("InitialState")] public Collection States { get { if (this.states == null) { this.states = new ValidatingCollection { // disallow null values OnAddValidationCallback = item => { if (item == null) { throw FxTrace.Exception.AsError(new ArgumentNullException("item")); } }, }; } return this.states; } } /// /// Gets Variables which can be used within StateMachine scope. /// [DependsOn("States")] public Collection Variables { get { if (this.variables == null) { this.variables = new ValidatingCollection { // disallow null values OnAddValidationCallback = item => { if (item == null) { throw FxTrace.Exception.AsError(new ArgumentNullException("item")); } }, }; } return this.variables; } } uint PassNumber { get; set; } /// /// Perform State Machine validation, in the following order: /// 1. Mark all states in States collection with an Id. /// 2. Traverse all states via declared transitions, and mark reachable states. /// 3. Validate transitions, states, and state machine /// Finally, declare arguments and variables of state machine, and declare states and transitions as activitydelegates. /// /// NativeActivityMetadata reference protected override void CacheMetadata(NativeActivityMetadata metadata) { // cleanup this.internalStateFuncs.Clear(); this.internalStates.Clear(); // clear Ids and Flags via transitions this.PassNumber++; this.TraverseViaTransitions(ClearState, ClearTransition); // clear Ids and Flags of all containing State references. this.PassNumber++; this.TraverseStates( (NativeActivityMetadata m, Collection states) => { ClearStates(states); }, (NativeActivityMetadata m, State state) => { ClearTransitions(state); }, metadata, checkReached: false); // Mark via states and do some check this.PassNumber++; this.TraverseStates( this.MarkStatesViaChildren, (NativeActivityMetadata m, State state) => { MarkTransitionsInState(state); }, metadata, checkReached: false); this.PassNumber++; // Mark via transition this.TraverseViaTransitions(delegate(State state) { MarkStateViaTransition(state); }, null); // Do validation via children // need not check the violation of state which is not reached this.PassNumber++; this.TraverseViaTransitions( (State state) => { ValidateTransitions(metadata, state); }, actionForTransition: null); this.PassNumber++; this.TraverseStates( ValidateStates, (NativeActivityMetadata m, State state) => { if (!state.Reachable) { // log validation for states that are not reachable in the previous pass. ValidateTransitions(m, state); } }, metadata: metadata, checkReached: true); // Validate the root state machine itself this.ValidateStateMachine(metadata); this.ProcessStates(metadata); metadata.AddImplementationVariable(this.eventManager); foreach (Variable variable in this.Variables) { metadata.AddVariable(variable); } metadata.AddDefaultExtensionProvider(getDefaultExtension); } /// /// Execution of StateMachine /// /// NativeActivityContext reference [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "The context is used by workflow runtime. The parameter should be fine.")] protected override void Execute(NativeActivityContext context) { // We view the duration before moving to initial state is on transition. StateMachineEventManager localEventManager = this.eventManager.Get(context); localEventManager.OnTransition = true; localEventManager.CurrentBeingProcessedEvent = null; int index = StateMachineIdHelper.GetChildStateIndex(RootId, this.InitialState.StateId); context.ScheduleFunc( this.internalStateFuncs[index], localEventManager, this.onStateComplete); } protected override void OnCreateDynamicUpdateMap(NativeActivityUpdateMapMetadata metadata, Activity originalActivity) { // enable dynamic update in state machine metadata.AllowUpdateInsideThisActivity(); } static void MarkTransitionsInState(State state) { if (state.Transitions.Count > 0) { for (int i = 0; i < state.Transitions.Count; i++) { Transition transition = state.Transitions[i]; if (!string.IsNullOrEmpty(state.StateId)) { transition.Id = StateMachineIdHelper.GenerateTransitionId(state.StateId, i); } } } } static void MarkStateViaTransition(State state) { state.Reachable = true; } static void ClearStates(Collection states) { foreach (State state in states) { ClearState(state); } } static void ClearState(State state) { state.StateId = null; state.Reachable = false; state.ClearInternalState(); } static void ClearTransitions(State state) { foreach (Transition transition in state.Transitions) { ClearTransition(transition); } } static void ClearTransition(Transition transition) { transition.Source = null; } static void ValidateStates(NativeActivityMetadata metadata, Collection states) { foreach (State state in states) { // only validate reached state. ValidateState(metadata, state); } } static void ValidateState(NativeActivityMetadata metadata, State state) { Fx.Assert(!string.IsNullOrEmpty(state.StateId), "StateId should have been set."); if (state.Reachable) { if (state.IsFinal) { if (state.Exit != null) { metadata.AddValidationError(new ValidationError( SR.FinalStateCannotHaveProperty(state.DisplayName, ExitProperty), isWarning: false, propertyName: string.Empty, sourceDetail: state)); } if (state.Transitions.Count > 0) { metadata.AddValidationError(new ValidationError( SR.FinalStateCannotHaveTransition(state.DisplayName), isWarning: false, propertyName: string.Empty, sourceDetail: state)); } } else { if (state.Transitions.Count == 0) { metadata.AddValidationError(new ValidationError( SR.SimpleStateMustHaveOneTransition(state.DisplayName), isWarning: false, propertyName: string.Empty, sourceDetail: state)); } } } } static void ValidateTransitions(NativeActivityMetadata metadata, State currentState) { Collection transitions = currentState.Transitions; HashSet conditionalTransitionTriggers = new HashSet(); Dictionary> unconditionalTransitionMapping = new Dictionary>(); foreach (Transition transition in transitions) { if (transition.Source != null) { metadata.AddValidationError(new ValidationError( SR.TransitionCannotBeAddedTwice(transition.DisplayName, currentState.DisplayName, transition.Source.DisplayName), isWarning: false, propertyName: string.Empty, sourceDetail: transition)); continue; } else { transition.Source = currentState; } if (transition.To == null) { metadata.AddValidationError(new ValidationError( SR.TransitionTargetCannotBeNull(transition.DisplayName, currentState.DisplayName), isWarning: false, propertyName: string.Empty, sourceDetail: transition)); } else if (string.IsNullOrEmpty(transition.To.StateId)) { metadata.AddValidationError(new ValidationError( SR.StateNotBelongToAnyParent( transition.DisplayName, transition.To.DisplayName), isWarning: false, propertyName: string.Empty, sourceDetail: transition)); } Activity triggerActivity = transition.ActiveTrigger; if (transition.Condition == null) { if (!unconditionalTransitionMapping.ContainsKey(triggerActivity)) { unconditionalTransitionMapping.Add(triggerActivity, new List()); } unconditionalTransitionMapping[triggerActivity].Add(transition); } else { conditionalTransitionTriggers.Add(triggerActivity); } } foreach (KeyValuePair> unconditionalTransitions in unconditionalTransitionMapping) { if (conditionalTransitionTriggers.Contains(unconditionalTransitions.Key) || unconditionalTransitions.Value.Count > 1) { foreach (Transition transition in unconditionalTransitions.Value) { if (transition.Trigger != null) { metadata.AddValidationError(new ValidationError( SR.UnconditionalTransitionShouldNotShareTriggersWithOthers( transition.DisplayName, currentState.DisplayName, transition.Trigger.DisplayName), isWarning: false, propertyName: string.Empty, sourceDetail: currentState)); } else { // Null Trigger metadata.AddValidationError(new ValidationError( SR.UnconditionalTransitionShouldNotShareNullTriggersWithOthers( transition.DisplayName, currentState.DisplayName), isWarning: false, propertyName: string.Empty, sourceDetail: currentState)); } } } } } static StateMachineExtension GetStateMachineExtension() { return new StateMachineExtension(); } /// /// Create internal states /// /// NativeActivityMetadata reference. private void ProcessStates(NativeActivityMetadata metadata) { // remove duplicate state in the collection during evaluation IEnumerable distinctStates = this.states.Distinct(); foreach (State state in distinctStates) { InternalState internalState = state.InternalState; this.internalStates.Add(internalState); DelegateInArgument eventManager = new DelegateInArgument(); internalState.EventManager = eventManager; ActivityFunc activityFunc = new ActivityFunc { Argument = eventManager, Handler = internalState, }; if (state.Reachable) { // If this state is not reached, we should not add it as child because it's even not well validated. metadata.AddDelegate(activityFunc, /* origin = */ state); } this.internalStateFuncs.Add(activityFunc); } } void OnStateComplete(NativeActivityContext context, ActivityInstance completedInstance, string result) { if (StateMachineIdHelper.IsAncestor(RootId, result)) { int index = StateMachineIdHelper.GetChildStateIndex(RootId, result); context.ScheduleFunc( this.internalStateFuncs[index], this.eventManager.Get(context), this.onStateComplete); } } void ValidateStateMachine(NativeActivityMetadata metadata) { if (this.InitialState == null) { metadata.AddValidationError(SR.StateMachineMustHaveInitialState(this.DisplayName)); } else { if (this.InitialState.IsFinal) { Fx.Assert(!string.IsNullOrEmpty(this.InitialState.StateId), "StateId should have get set on the initialState."); metadata.AddValidationError(new ValidationError( SR.InitialStateCannotBeFinalState(this.InitialState.DisplayName), isWarning: false, propertyName: string.Empty, sourceDetail: this.InitialState)); } if (!this.States.Contains(this.InitialState)) { Fx.Assert(string.IsNullOrEmpty(this.InitialState.StateId), "Initial state would not have an id because it is not in the States collection."); metadata.AddValidationError(SR.InitialStateNotInStatesCollection(this.InitialState.DisplayName)); } } } void TraverseStates( Action> actionForStates, Action actionForTransitions, NativeActivityMetadata metadata, bool checkReached) { if (actionForStates != null) { actionForStates(metadata, this.States); } uint passNumber = this.PassNumber; IEnumerable distinctStates = this.States.Distinct(); foreach (State state in distinctStates) { if (!checkReached || state.Reachable) { state.PassNumber = passNumber; if (actionForTransitions != null) { actionForTransitions(metadata, state); } } } } void MarkStatesViaChildren(NativeActivityMetadata metadata, Collection states) { if (states.Count > 0) { for (int i = 0; i < states.Count; i++) { State state = states[i]; if (string.IsNullOrEmpty(state.StateId)) { state.StateId = StateMachineIdHelper.GenerateStateId(RootId, i); state.StateMachineName = this.DisplayName; } else { // the state has been makred already: a duplicate state is found metadata.AddValidationError(new ValidationError( SR.StateCannotBeAddedTwice(state.DisplayName), isWarning: false, propertyName: string.Empty, sourceDetail: state)); } } } } void TraverseViaTransitions(Action actionForState, Action actionForTransition) { Stack stack = new Stack(); stack.Push(this.InitialState); uint passNumber = this.PassNumber; while (stack.Count > 0) { State currentState = stack.Pop(); if (currentState == null || currentState.PassNumber == passNumber) { continue; } currentState.PassNumber = passNumber; if (actionForState != null) { actionForState(currentState); } foreach (Transition transition in currentState.Transitions) { if (actionForTransition != null) { actionForTransition(transition); } stack.Push(transition.To); } } } /// /// Originally, the Default value for StateMachineEventManager variable in StateMachine activity, /// is initialized via a LambdaValue activity. However, PartialTrust environment does not support /// LambdaValue activity that references any local variables or non-public members. /// The recommended approach is to convert the LambdaValue to an equivalent internal CodeActivity. /// sealed class StateMachineEventManagerFactory : CodeActivity { protected override void CacheMetadata(CodeActivityMetadata metadata) { if (this.Result == null) { // metdata.Bind uses reflection if the argument property has a value of null. // So by forcing the argument property to have a non-null value, it avoids reflection. // Otherwise it would use reflection to initializer Result and would fail Partial Trust. this.Result = new OutArgument(); } RuntimeArgument eventManagerArgument = new RuntimeArgument("Result", this.ResultType, ArgumentDirection.Out); metadata.Bind(this.Result, eventManagerArgument); metadata.AddArgument(eventManagerArgument); } protected override StateMachineEventManager Execute(CodeActivityContext context) { return new StateMachineEventManager(); } } } }