[corlib] Update ValueTuple implementation
[mono.git] / mcs / class / referencesource / System.Activities.Presentation / System.Activities.Presentation / System / Activities / Presentation / Metadata / ActivityArgumentHelper.cs
1 //----------------------------------------------------------------
2 // <copyright company="Microsoft Corporation">
3 //     Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>
5 //----------------------------------------------------------------
6
7 namespace System.Activities.Presentation.Metadata
8 {
9     using System.Activities.Presentation.Model;
10     using System.Activities.Presentation.Validation;
11     using System.Activities.Statements;
12     using System.Activities.Validation;
13     using System.Collections.Generic;
14     using System.Globalization;
15     using System.Linq;
16     using System.Runtime;
17
18     /// <summary>
19     /// A helper class to provide additional functionalities regarding Activity arguments.
20     /// </summary>
21     public static class ActivityArgumentHelper
22     {
23         private static Dictionary<Type, Func<Activity, IEnumerable<ArgumentAccessor>>> argumentAccessorsGenerators = new Dictionary<Type, Func<Activity, IEnumerable<ArgumentAccessor>>>();
24
25         /// <summary>
26         /// Registers with an activity type a function to generate a list of ArgumentAccessors.
27         /// </summary>
28         /// <param name="activityType">The activity type.</param>
29         /// <param name="argumentAccessorsGenerator">A function which takes in an activity instance (of type activityType) and returns a list of ArgumentAccessors.</param>
30         public static void RegisterAccessorsGenerator(Type activityType, Func<Activity, IEnumerable<ArgumentAccessor>> argumentAccessorsGenerator)
31         {
32             if (activityType == null)
33             {
34                 throw FxTrace.Exception.ArgumentNull("activityType");
35             }
36
37             if (argumentAccessorsGenerator == null)
38             {
39                 throw FxTrace.Exception.ArgumentNull("argumentAccessorsGenerator");
40             }
41
42             // IsAssignableFrom() returns true if activityType is the generic type definition inheriting from Activity.
43             if (!typeof(Activity).IsAssignableFrom(activityType))
44             {
45                 throw FxTrace.Exception.Argument("activityType", string.Format(CultureInfo.CurrentCulture, SR.TypeDoesNotInheritFromActivity, activityType.Name));
46             }
47
48             argumentAccessorsGenerators[activityType] = argumentAccessorsGenerator;
49         }
50
51         internal static bool TryGetArgumentAccessorsGenerator(Type activityType, out Func<Activity, IEnumerable<ArgumentAccessor>> argumentAccessorsGenerator)
52         {
53             Fx.Assert(activityType != null, "activityType cannot be null.");
54
55             bool argumentAccessorsGeneratorFound = argumentAccessorsGenerators.TryGetValue(activityType, out argumentAccessorsGenerator);
56             if (!argumentAccessorsGeneratorFound && activityType.IsGenericType && !activityType.IsGenericTypeDefinition)
57             {
58                 argumentAccessorsGeneratorFound = argumentAccessorsGenerators.TryGetValue(activityType.GetGenericTypeDefinition(), out argumentAccessorsGenerator);
59             }
60
61             return argumentAccessorsGeneratorFound;
62         }
63
64         internal static void UpdateInvalidArgumentsIfNecessary(object sender, ValidationService.ErrorsMarkedEventArgs args)
65         {
66             if (args.Reason != ValidationReason.ModelChange)
67             {
68                 return;
69             }
70
71             if (args.Errors.Count == 0)
72             {
73                 return;
74             }
75
76             using (EditingScope editingScope = args.ModelTreeManager.CreateEditingScope(string.Empty))
77             {
78                 // Prevent the validation -> fix arguments -> validation loop.
79                 editingScope.SuppressUndo = true;
80
81                 // Suppress validation. We will do it ourselves (see below)
82                 editingScope.SuppressValidationOnComplete = true;
83
84                 // Re-compile erroreous expressions to see if update is necessary
85                 ValidationService validationService = args.Context.Services.GetRequiredService<ValidationService>();
86                 ArgumentAccessorWrapperCache argumentAccessorWrapperCache = new ArgumentAccessorWrapperCache();
87                 List<ExpressionReplacement> expressionReplacements = ComputeExpressionReplacements(args.Errors.Select(error => error.Source).OfType<ActivityWithResult>(), args.Context, argumentAccessorWrapperCache);
88                 bool argumentReplacementOccurred = false;
89                 if (expressionReplacements.Count > 0)
90                 {
91                     try
92                     {
93                         foreach (ExpressionReplacement expressionReplacement in expressionReplacements)
94                         {
95                             if (expressionReplacement.TryReplaceArgument(args.ModelTreeManager, validationService))
96                             {
97                                 argumentReplacementOccurred = true;
98                             }
99                         }
100
101                         if (argumentReplacementOccurred)
102                         {
103                             args.Handled = true;
104                             editingScope.Complete();
105                         }
106                     }
107                     catch (Exception e)
108                     {
109                         if (Fx.IsFatal(e))
110                         {
111                             throw;
112                         }
113
114                         // We handle exception here instead of letting WF Designer handle it, so that the validation below will run even
115                         // if any of the ArgumentAccessor.Setter methods throw exceptions.
116                         ErrorReporting.ShowErrorMessage(e);
117                     }
118
119                     // Since any pending validation will be canceled if argument replacement occured (=has model change), we need to re-validate the workflow.
120                     // We suppressed validation upon EditingScope completion and do it ourselves, because
121                     // the argument replacement could have been done directly to the underlying activity instance, rather than thru ModelItem.
122                     if (argumentReplacementOccurred)
123                     {
124                         validationService.ValidateWorkflow();
125                     }
126                 }
127             }
128         }
129
130         internal static List<ExpressionReplacement> ComputeExpressionReplacements(IEnumerable<ActivityWithResult> expressions, EditingContext context, ArgumentAccessorWrapperCache argumentAccessorWrapperCache)
131         {
132             Fx.Assert(expressions != null, "expressions cannot be null.");
133             Fx.Assert(context != null, "context cannot be null.");
134             Fx.Assert(argumentAccessorWrapperCache != null, "argumentAccessorWrapperCache cannot be null.");
135
136             HashSet<Assign> assignsWithRValueToFix = new HashSet<Assign>();
137             Dictionary<Assign, Type> assignLValueTypes = new Dictionary<Assign, Type>();
138             List<ExpressionReplacement> expressionReplacements = new List<ExpressionReplacement>();
139             foreach (ActivityWithResult expression in expressions)
140             {
141                 Activity parentActivity = ValidationService.GetParent(expression);
142                 if (parentActivity == null)
143                 {
144                     continue;
145                 }
146
147                 Assign assignActivity = parentActivity as Assign;
148                 if (assignActivity != null && !ExpressionHelper.IsGenericLocationExpressionType(expression))
149                 {
150                     assignsWithRValueToFix.Add(assignActivity);
151                 }
152                 else
153                 {
154                     ExpressionReplacement expressionReplacement = ComputeExpressionReplacement(expression, parentActivity, context, argumentAccessorWrapperCache);
155                     if (expressionReplacement != null)
156                     {
157                         expressionReplacements.Add(expressionReplacement);
158                         if (assignActivity != null)
159                         {
160                             Type expectedReturnType = expressionReplacement.NewArgument.ArgumentType;
161                             assignLValueTypes[assignActivity] = expectedReturnType;
162                         }
163                     }
164                 }
165             }
166
167             // Special handle Assign R-Values: Assign.To.ArgumentType must be the same as Assign.Value.ArgumentType.
168             foreach (Assign assignWithRValueToFix in assignsWithRValueToFix)
169             {
170                 Type expectedReturnType;
171                 if (assignLValueTypes.TryGetValue(assignWithRValueToFix, out expectedReturnType))
172                 {
173                     assignLValueTypes.Remove(assignWithRValueToFix);
174                 }
175                 else if (assignWithRValueToFix.To != null)
176                 {
177                     expectedReturnType = assignWithRValueToFix.To.ArgumentType;
178                 }
179
180                 ExpressionReplacement expressionReplacement = ComputeExpressionReplacement(assignWithRValueToFix.Value.Expression, assignWithRValueToFix, context, argumentAccessorWrapperCache, expectedReturnType);
181                 if (expressionReplacement != null)
182                 {
183                     expressionReplacements.Add(expressionReplacement);
184                 }
185             }
186
187             // These Assign activities have their L-value argument (To) changed but not the R-value argument (Value).
188             // Now make sure that the R-value arguments are compatible with the L-value argument.
189             foreach (KeyValuePair<Assign, Type> kvp in assignLValueTypes)
190             {
191                 Assign remainingAssign = kvp.Key;
192                 Type expectedReturnType = kvp.Value;
193                 if (remainingAssign.Value != null && remainingAssign.Value.Expression != null)
194                 {
195                     ActivityWithResult expression = remainingAssign.Value.Expression;
196                     if (expression.ResultType != expectedReturnType)
197                     {
198                         ExpressionReplacement expressionReplacement = ComputeExpressionReplacement(expression, remainingAssign, context, argumentAccessorWrapperCache, expectedReturnType);
199                         if (expressionReplacement != null)
200                         {
201                             expressionReplacements.Add(expressionReplacement);
202                         }
203                     }
204                 }
205             }
206
207             return expressionReplacements;
208         }
209
210         internal static ExpressionReplacement ComputeExpressionReplacement(ActivityWithResult expression, Activity parentActivity, EditingContext context, ArgumentAccessorWrapperCache argumentAccessorWrapperCache, Type preferredReturnType = null)
211         {
212             Fx.Assert(expression != null, "expressions cannot be null.");
213             Fx.Assert(parentActivity != null, "parentActivity cannot be null.");
214             Fx.Assert(context != null, "context cannot be null.");
215             Fx.Assert(argumentAccessorWrapperCache != null, "argumentAccessorWrapperCache cannot be null.");
216
217             IEnumerable<ArgumentAccessorWrapper> argumentAccessorWrappers = argumentAccessorWrapperCache.GetArgumentAccessorWrappers(parentActivity);
218             if (argumentAccessorWrappers != null)
219             {
220                 ArgumentAccessorWrapper argumentAccessorWrapper = argumentAccessorWrappers.FirstOrDefault(wrapper => object.ReferenceEquals(wrapper.Argument.Expression, expression));
221                 if (argumentAccessorWrapper != null)
222                 {
223                     bool isLocationExpression = ExpressionHelper.IsGenericLocationExpressionType(expression);
224                     bool canInferType = true;
225                     Type expectedReturnType;
226                     ActivityWithResult morphedExpression;
227                     if (preferredReturnType != null)
228                     {
229                         expectedReturnType = preferredReturnType;
230                     }
231                     else
232                     {
233                         canInferType = ExpressionHelper.TryInferReturnType(expression, context, out expectedReturnType);
234                     }
235
236                     if (canInferType && expectedReturnType != null && ExpressionHelper.TryMorphExpression(expression, isLocationExpression, expectedReturnType, context, out morphedExpression))
237                     {
238                         Type expressionResultType = isLocationExpression ? expression.ResultType.GetGenericArguments()[0] : expression.ResultType;
239                         if (expressionResultType != expectedReturnType)
240                         {
241                             Argument newArgument = Argument.Create(expectedReturnType, argumentAccessorWrapper.Argument.Direction);
242                             newArgument.Expression = morphedExpression;
243                             return new ExpressionReplacement(expression, argumentAccessorWrapper.Argument, newArgument, argumentAccessorWrapper.ArgumentAccessor);
244                         }
245                     }
246                 }
247             }
248
249             return null;
250         }
251
252         internal sealed class ArgumentAccessorWrapper
253         {
254             public ArgumentAccessorWrapper(ArgumentAccessor argumentAccessor, Argument argument)
255             {
256                 Fx.Assert(argumentAccessor != null, "argumentAccessor cannot be null.");
257                 Fx.Assert(argument != null, "argument cannot be null.");
258
259                 this.ArgumentAccessor = argumentAccessor;
260                 this.Argument = argument;
261             }
262
263             public ArgumentAccessor ArgumentAccessor { get; private set; }
264
265             public Argument Argument { get; private set; }
266         }
267
268         // This cache ensures that during the entire argument-fixing process (i.e. within UpdateInvalidArgumentsIfNecessary):
269         // 1) An argument accessor generator function is called on an activity instance at most once.
270         // 2) An argument accessor Getter function is called on an activity instance at most once.
271         internal sealed class ArgumentAccessorWrapperCache
272         {
273             private Dictionary<Activity, List<ArgumentAccessorWrapper>> argumentAccessorWrappersCache;
274
275             public ArgumentAccessorWrapperCache()
276             {
277                 this.argumentAccessorWrappersCache = new Dictionary<Activity, List<ArgumentAccessorWrapper>>();
278             }
279
280             public List<ArgumentAccessorWrapper> GetArgumentAccessorWrappers(Activity activity)
281             {
282                 Fx.Assert(activity != null, "activity cannot be null.");
283
284                 List<ArgumentAccessorWrapper> argumentAccessorWrappers = null;
285                 if (!this.argumentAccessorWrappersCache.TryGetValue(activity, out argumentAccessorWrappers))
286                 {
287                     Func<Activity, IEnumerable<ArgumentAccessor>> argumentAccessorsGenerator;
288                     if (ActivityArgumentHelper.TryGetArgumentAccessorsGenerator(activity.GetType(), out argumentAccessorsGenerator))
289                     {
290                         IEnumerable<ArgumentAccessor> argumentAccessors = argumentAccessorsGenerator(activity);
291                         if (argumentAccessors != null)
292                         {
293                             argumentAccessorWrappers = new List<ArgumentAccessorWrapper>();
294                             foreach (ArgumentAccessor argumentAccessor in argumentAccessors)
295                             {
296                                 if (argumentAccessor != null && argumentAccessor.Getter != null)
297                                 {
298                                     Argument argument = argumentAccessor.Getter(activity);
299                                     if (argument != null)
300                                     {
301                                         ArgumentAccessorWrapper argumentAccessorWrapper = new ArgumentAccessorWrapper(argumentAccessor, argument);
302                                         argumentAccessorWrappers.Add(argumentAccessorWrapper);
303                                     }
304                                 }
305                             }
306
307                             this.argumentAccessorWrappersCache.Add(activity, argumentAccessorWrappers);
308                         }
309                     }
310                 }
311
312                 return argumentAccessorWrappers;
313             }
314         }
315
316         internal sealed class ExpressionReplacement
317         {
318             public ExpressionReplacement(ActivityWithResult expressionToReplace, Argument oldArgument, Argument newArgument, ArgumentAccessor argumentAccessor)
319             {
320                 Fx.Assert(expressionToReplace != null, "expressionToReplace cannot be null.");
321                 Fx.Assert(oldArgument != null, "oldArgument cannot be null.");
322                 Fx.Assert(newArgument != null, "newArgument cannot be null.");
323                 Fx.Assert(argumentAccessor != null, "argumentAccessor cannot be null.");
324
325                 this.ExpressionToReplace = expressionToReplace;
326                 this.OldArgument = oldArgument;
327                 this.NewArgument = newArgument;
328                 this.ArgumentAccessor = argumentAccessor;
329             }
330
331             public ActivityWithResult ExpressionToReplace
332             {
333                 get;
334                 private set;
335             }
336
337             public Argument OldArgument
338             {
339                 get;
340                 private set;
341             }
342
343             public Argument NewArgument
344             {
345                 get;
346                 private set;
347             }
348
349             public ArgumentAccessor ArgumentAccessor
350             {
351                 get;
352                 private set;
353             }
354
355             public bool TryReplaceArgument(ModelTreeManager modelTreeManager, ValidationService validationService)
356             {
357                 Fx.Assert(modelTreeManager != null, "modelTreeManager cannot be null.");
358                 Fx.Assert(validationService != null, "validationService cannot be null.");
359
360                 ModelItem expressionModelItem = modelTreeManager.GetModelItem(this.ExpressionToReplace);
361                 if (expressionModelItem != null)
362                 {
363                     ModelItem argumentModelItem = expressionModelItem.Parent;
364                     ModelItem parentObject = argumentModelItem.Parent;
365
366                     if (argumentModelItem.Source != null)
367                     {
368                         ModelProperty argumentProperty = parentObject.Properties[argumentModelItem.Source.Name];
369                         Type argumentPropertyType = argumentProperty.PropertyType;
370                         if (argumentPropertyType == typeof(InArgument) || argumentPropertyType == typeof(OutArgument) || argumentPropertyType == typeof(InOutArgument))
371                         {
372                             ModelItem oldArgumentModel = argumentProperty.Value;
373                             ModelItem newArgumentModel = argumentProperty.SetValue(this.NewArgument);
374
375                             // Make sure argument.Expression is wrapped in ModelItem as well
376                             ModelItem newExpressionModel = newArgumentModel.Properties["Expression"].Value;
377
378                             return true;
379                         }
380                     }
381                 }
382                 else
383                 {
384                     Activity parentActivity = ValidationService.GetParent(this.ExpressionToReplace);
385                     if (this.ArgumentAccessor.Setter != null)
386                     {
387                         try
388                         {
389                             validationService.DeactivateValidation();
390                             this.ArgumentAccessor.Setter(parentActivity, this.NewArgument);
391                             return true;
392                         }
393                         finally
394                         {
395                             validationService.ActivateValidation();
396                         }
397                     }
398                 }
399
400                 return false;
401             }
402         }
403     }
404 }