e79aa3c0ed
Former-commit-id: a2155e9bd80020e49e72e86c44da02a8ac0e57a4
620 lines
24 KiB
C#
620 lines
24 KiB
C#
//------------------------------------------------------------------------------
|
|
// <copyright file="StateMachine.cs" company="Microsoft">
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// </copyright>
|
|
//------------------------------------------------------------------------------
|
|
|
|
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;
|
|
|
|
/// <summary>
|
|
/// This class represents a StateMachine which contains States and Variables.
|
|
/// </summary>
|
|
[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<StateMachineExtension> getDefaultExtension = new Func<StateMachineExtension>(GetStateMachineExtension);
|
|
|
|
// states in root level of StateMachine
|
|
Collection<State> states;
|
|
|
|
// variables used in StateMachine
|
|
Collection<Variable> variables;
|
|
|
|
// internal representations of states
|
|
Collection<InternalState> internalStates;
|
|
|
|
// ActivityFuncs who call internal activities
|
|
Collection<ActivityFunc<StateMachineEventManager, string>> internalStateFuncs;
|
|
|
|
// Callback when a state completes
|
|
CompletionCallback<string> 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<StateMachineEventManager> eventManager;
|
|
|
|
/// <summary>
|
|
/// It's constructor.
|
|
/// </summary>
|
|
public StateMachine()
|
|
{
|
|
this.internalStates = new Collection<InternalState>();
|
|
this.internalStateFuncs = new Collection<ActivityFunc<StateMachineEventManager, string>>();
|
|
this.eventManager = new Variable<StateMachineEventManager> { Name = "EventManager", Default = new StateMachineEventManagerFactory() };
|
|
this.onStateComplete = new CompletionCallback<string>(this.OnStateComplete);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the start point of the StateMachine.
|
|
/// </summary>
|
|
[DefaultValue(null)]
|
|
public State InitialState
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all root level States in the StateMachine.
|
|
/// </summary>
|
|
[DependsOn("InitialState")]
|
|
public Collection<State> States
|
|
{
|
|
get
|
|
{
|
|
if (this.states == null)
|
|
{
|
|
this.states = new ValidatingCollection<State>
|
|
{
|
|
// disallow null values
|
|
OnAddValidationCallback = item =>
|
|
{
|
|
if (item == null)
|
|
{
|
|
throw FxTrace.Exception.AsError(new ArgumentNullException("item"));
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
return this.states;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets Variables which can be used within StateMachine scope.
|
|
/// </summary>
|
|
[DependsOn("States")]
|
|
public Collection<Variable> Variables
|
|
{
|
|
get
|
|
{
|
|
if (this.variables == null)
|
|
{
|
|
this.variables = new ValidatingCollection<Variable>
|
|
{
|
|
// disallow null values
|
|
OnAddValidationCallback = item =>
|
|
{
|
|
if (item == null)
|
|
{
|
|
throw FxTrace.Exception.AsError(new ArgumentNullException("item"));
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
return this.variables;
|
|
}
|
|
}
|
|
|
|
uint PassNumber
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="metadata">NativeActivityMetadata reference</param>
|
|
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<State> 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<StateMachineExtension>(getDefaultExtension);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Execution of StateMachine
|
|
/// </summary>
|
|
/// <param name="context">NativeActivityContext reference</param>
|
|
[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<StateMachineEventManager, string>(
|
|
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<State> 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<State> 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<Transition> transitions = currentState.Transitions;
|
|
HashSet<Activity> conditionalTransitionTriggers = new HashSet<Activity>();
|
|
Dictionary<Activity, List<Transition>> unconditionalTransitionMapping = new Dictionary<Activity, List<Transition>>();
|
|
|
|
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<Transition>());
|
|
}
|
|
|
|
unconditionalTransitionMapping[triggerActivity].Add(transition);
|
|
}
|
|
else
|
|
{
|
|
conditionalTransitionTriggers.Add(triggerActivity);
|
|
}
|
|
}
|
|
|
|
foreach (KeyValuePair<Activity, List<Transition>> 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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create internal states
|
|
/// </summary>
|
|
/// <param name="metadata">NativeActivityMetadata reference.</param>
|
|
private void ProcessStates(NativeActivityMetadata metadata)
|
|
{
|
|
// remove duplicate state in the collection during evaluation
|
|
IEnumerable<State> distinctStates = this.states.Distinct();
|
|
|
|
foreach (State state in distinctStates)
|
|
{
|
|
InternalState internalState = state.InternalState;
|
|
this.internalStates.Add(internalState);
|
|
|
|
DelegateInArgument<StateMachineEventManager> eventManager = new DelegateInArgument<Statements.StateMachineEventManager>();
|
|
internalState.EventManager = eventManager;
|
|
|
|
ActivityFunc<StateMachineEventManager, string> activityFunc = new ActivityFunc<StateMachineEventManager, string>
|
|
{
|
|
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<StateMachineEventManager, string>(
|
|
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<NativeActivityMetadata, Collection<State>> actionForStates,
|
|
Action<NativeActivityMetadata, State> actionForTransitions,
|
|
NativeActivityMetadata metadata,
|
|
bool checkReached)
|
|
{
|
|
if (actionForStates != null)
|
|
{
|
|
actionForStates(metadata, this.States);
|
|
}
|
|
|
|
uint passNumber = this.PassNumber;
|
|
|
|
IEnumerable<State> 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<State> 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<State> actionForState, Action<Transition> actionForTransition)
|
|
{
|
|
Stack<State> stack = new Stack<State>();
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
sealed class StateMachineEventManagerFactory : CodeActivity<StateMachineEventManager>
|
|
{
|
|
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<StateMachineEventManager>();
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|