- CheckboxList's LabelFor now takes attributes as optional argument
[castle.git] / MonoRail / Castle.MonoRail.Framework / SmartDispatcherController.cs
blobbfaaeb13c8be5f0d84c224a8d4f9df9ee9d9a4e5
1 // Copyright 2004-2008 Castle Project - http://www.castleproject.org/
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
15 namespace Castle.MonoRail.Framework
17 using System;
18 using System.Collections.Generic;
19 using System.Reflection;
20 using System.Collections;
21 using System.Collections.Specialized;
22 using Castle.Components.Binder;
24 /// <summary>
25 /// Specialization of <see cref="Controller"/> that tries
26 /// to match the request params to method arguments.
27 /// </summary>
28 /// <remarks>
29 /// You don't even need to always use databinding within
30 /// arguments. <see cref="BindObject(ParamStore,Type,string)"/>
31 /// and <see cref="BindObjectInstance(object,string)"/>
32 /// provides the same functionality to be used in place.
33 /// </remarks>
34 public abstract class SmartDispatcherController : Controller
36 private IDataBinder binder;
38 /// <summary>
39 /// Represents the errors associated with an instance bound.
40 /// </summary>
41 protected IDictionary<object, ErrorList> boundInstances = new Dictionary<object, ErrorList>();
43 /// <summary>
44 /// Initializes a new instance of the <see cref="SmartDispatcherController"/> class.
45 /// </summary>
46 protected SmartDispatcherController() : this(new DataBinder())
50 /// <summary>
51 /// Initializes a new instance of the <see cref="SmartDispatcherController"/> class.
52 /// </summary>
53 /// <param name="binder">The binder.</param>
54 protected SmartDispatcherController(IDataBinder binder)
56 this.binder = binder;
59 /// <summary>
60 /// Gets the binder.
61 /// </summary>
62 /// <value>The binder.</value>
63 public IDataBinder Binder
65 get { return binder; }
68 /// <summary>
69 /// Constructs the parameters for the action and invokes it.
70 /// </summary>
71 /// <param name="method">The method.</param>
72 /// <param name="request">The request.</param>
73 /// <param name="extraArgs">The extra args.</param>
74 /// <returns></returns>
75 protected override object InvokeMethod(MethodInfo method, IRequest request, IDictionary<string, object> extraArgs)
77 ParameterInfo[] parameters = method.GetParameters();
79 object[] methodArgs = BuildMethodArguments(parameters, request, extraArgs);
81 return method.Invoke(this, methodArgs);
84 /// <summary>
85 /// Uses a simple heuristic to select the best method -- especially in the
86 /// case of method overloads.
87 /// </summary>
88 /// <param name="action">The action name</param>
89 /// <param name="actions">The avaliable actions</param>
90 /// <param name="request">The request instance</param>
91 /// <param name="actionArgs">The custom arguments for the action</param>
92 /// <returns></returns>
93 protected override MethodInfo SelectMethod(string action, IDictionary actions, IRequest request, IDictionary<string, object> actionArgs)
95 object methods = actions[action];
97 // should check for single-option as soon as possible (performance improvement)
98 if (methods is MethodInfo) return (MethodInfo) methods;
100 ArrayList candidates = (ArrayList) methods;
102 if (candidates == null) return null;
104 return SelectBestCandidate((MethodInfo[]) candidates.ToArray(typeof(MethodInfo)),
105 request.Params, actionArgs);
108 /// <summary>
109 /// Selects the best method given the set of entries
110 /// avaliable on <paramref name="webParams"/> and <paramref name="actionArgs"/>
111 /// </summary>
112 /// <param name="candidates">The candidates.</param>
113 /// <param name="webParams">The web params.</param>
114 /// <param name="actionArgs">The custom action args.</param>
115 /// <returns></returns>
116 protected virtual MethodInfo SelectBestCandidate(MethodInfo[] candidates,
117 NameValueCollection webParams,
118 IDictionary<string, object> actionArgs)
120 if (candidates.Length == 1)
122 // There's nothing much to do in this situation
123 return candidates[0];
126 int lastMaxPoints = int.MinValue;
127 MethodInfo bestCandidate = null;
129 foreach(MethodInfo candidate in candidates)
131 int points = CalculatePoints(candidate, webParams, actionArgs);
133 if (lastMaxPoints < points)
135 lastMaxPoints = points;
136 bestCandidate = candidate;
140 return bestCandidate;
143 /// <summary>
144 /// Gets the name of the request parameter.
145 /// </summary>
146 /// <param name="param">The param.</param>
147 /// <returns></returns>
148 protected virtual String GetRequestParameterName(ParameterInfo param)
150 return param.Name;
153 /// <summary>
154 /// Uses a simplest algorithm to compute points for a method
155 /// based on parameters available, which in turn reflects
156 /// the best method is the one which the parameters will be
157 /// able to satistfy more arguments
158 /// </summary>
159 /// <param name="candidate">The method candidate</param>
160 /// <param name="webParams">Parameter source</param>
161 /// <param name="actionArgs">Extra parameters</param>
162 /// <returns></returns>
163 protected int CalculatePoints(MethodInfo candidate, NameValueCollection webParams, IDictionary<string, object> actionArgs)
165 int points = 0;
166 int matchCount = 0;
168 ParameterInfo[] parameters = candidate.GetParameters();
170 foreach(ParameterInfo param in parameters)
173 // If the param is decorated with an attribute that implements IParameterBinder
174 // then it calculates the points itself
177 object[] attributes = param.GetCustomAttributes(false);
179 String requestParameterName;
181 bool calculated = false;
183 foreach(object attr in attributes)
185 IParameterBinder actionParam = attr as IParameterBinder;
187 if (actionParam == null) continue;
189 points += actionParam.CalculateParamPoints(Context, this, ControllerContext, param);
190 calculated = true;
193 if (calculated) continue;
196 // Otherwise
199 requestParameterName = GetRequestParameterName(param);
201 Type parameterType = param.ParameterType;
202 bool usedActionArgs = false;
204 if ((actionArgs != null) && actionArgs.ContainsKey(requestParameterName))
206 object value = actionArgs[requestParameterName];
207 Type actionArgType = value != null ? value.GetType() : param.ParameterType;
209 bool exactMatch;
211 if (binder.Converter.CanConvert(parameterType, actionArgType, value, out exactMatch))
213 points += 10;
214 matchCount++;
215 usedActionArgs = true;
219 if (!usedActionArgs && binder.CanBindParameter(parameterType, requestParameterName, Request.ParamsNode))
221 points += 10;
222 matchCount++;
226 // the bonus should be nice only for disambiguation.
227 // otherwise, unmatched-parameterless-actions will always have
228 // the same weight as matched-single-parameter-actions.
229 if (matchCount == parameters.Length)
231 points += 5;
234 return points;
237 /// <summary>
238 /// Returns an array that hopefully fills the arguments of the selected action.
239 /// </summary>
240 /// <remarks>
241 /// Each parameter is inspected and we try to obtain an implementation of
242 /// <see cref="IParameterBinder"/> from the attributes the parameter have (if any).
243 /// If an implementation is found, it's used to fill the value for that parameter.
244 /// Otherwise we use simple conversion to obtain the value.
245 /// </remarks>
246 /// <param name="parameters">Parameters to obtain the values to</param>
247 /// <param name="request">The current request, which is the source to obtain the data</param>
248 /// <param name="actionArgs">Extra arguments to pass to the action.</param>
249 /// <returns>An array with the arguments values</returns>
250 protected virtual object[] BuildMethodArguments(ParameterInfo[] parameters, IRequest request, IDictionary<string, object> actionArgs)
252 object[] args = new object[parameters.Length];
253 String paramName = String.Empty;
254 String value = String.Empty;
258 for(int argIndex = 0; argIndex < args.Length; argIndex++)
261 // If the parameter is decorated with an attribute
262 // that implements IParameterBinder, it's up to it
263 // to convert itself
266 ParameterInfo param = parameters[argIndex];
267 paramName = GetRequestParameterName(param);
269 bool handled = false;
271 object[] attributes = param.GetCustomAttributes(false);
273 foreach(object attr in attributes)
275 IParameterBinder paramBinder = attr as IParameterBinder;
277 if (paramBinder != null)
279 args[argIndex] = paramBinder.Bind(Context, this, ControllerContext, param);
280 handled = true;
281 break;
286 // Otherwise we handle it
289 if (!handled)
291 object convertedVal= null;
292 bool conversionSucceeded = false;
294 if (actionArgs != null && actionArgs.ContainsKey(paramName))
296 object actionArg = actionArgs[paramName];
298 Type actionArgType = actionArg != null ? actionArg.GetType() : param.ParameterType;
300 convertedVal = binder.Converter.Convert(param.ParameterType, actionArgType, actionArg, out conversionSucceeded);
303 if (!conversionSucceeded)
305 convertedVal = binder.BindParameter(param.ParameterType, paramName, Request.ParamsNode);
308 args[argIndex] = convertedVal;
312 catch(FormatException ex)
314 throw new MonoRailException(
315 String.Format("Could not convert {0} to request type. " +
316 "Argument value is '{1}'", paramName, Params.Get(paramName)), ex);
318 catch(Exception ex)
320 throw new MonoRailException(
321 String.Format("Error building method arguments. " +
322 "Last param analyzed was {0} with value '{1}'", paramName, value), ex);
325 return args;
328 /// <summary>
329 /// Binds the object of the specified type using the given prefix.
330 /// </summary>
331 /// <param name="targetType">Type of the target.</param>
332 /// <param name="prefix">The prefix.</param>
333 /// <returns></returns>
334 protected object BindObject(Type targetType, String prefix)
336 return BindObject(ParamStore.Params, targetType, prefix);
339 /// <summary>
340 /// Binds the object of the specified type using the given prefix.
341 /// but only using the entries from the collection specified on the <paramref name="from"/>
342 /// </summary>
343 /// <param name="from">Restricts the data source of entries.</param>
344 /// <param name="targetType">Type of the target.</param>
345 /// <param name="prefix">The prefix.</param>
346 /// <returns></returns>
347 protected object BindObject(ParamStore from, Type targetType, String prefix)
349 return BindObject(from, targetType, prefix, null, null);
352 /// <summary>
353 /// Binds the object of the specified type using the given prefix.
354 /// but only using the entries from the collection specified on the <paramref name="from"/>
355 /// </summary>
356 /// <param name="from">From.</param>
357 /// <param name="targetType">Type of the target.</param>
358 /// <param name="prefix">The prefix.</param>
359 /// <param name="excludedProperties">The excluded properties, comma separated list.</param>
360 /// <param name="allowedProperties">The allowed properties, comma separated list.</param>
361 /// <returns></returns>
362 protected object BindObject(ParamStore from, Type targetType, String prefix, String excludedProperties,
363 String allowedProperties)
365 CompositeNode node = Request.ObtainParamsNode(from);
367 object instance = binder.BindObject(targetType, prefix, excludedProperties, allowedProperties, node);
369 boundInstances[instance] = binder.ErrorList;
370 PopulateValidatorErrorSummary(instance, binder.GetValidationSummary(instance));
372 return instance;
375 /// <summary>
376 /// Binds the object instance using the specified prefix.
377 /// </summary>
378 /// <param name="instance">The instance.</param>
379 /// <param name="prefix">The prefix.</param>
380 protected void BindObjectInstance(object instance, String prefix)
382 BindObjectInstance(instance, ParamStore.Params, prefix);
385 /// <summary>
386 /// Binds the object instance using the given prefix.
387 /// but only using the entries from the collection specified on the <paramref name="from"/>
388 /// </summary>
389 /// <param name="instance">The instance.</param>
390 /// <param name="from">From.</param>
391 /// <param name="prefix">The prefix.</param>
392 protected void BindObjectInstance(object instance, ParamStore from, String prefix)
394 CompositeNode node = Request.ObtainParamsNode(from);
396 binder.BindObjectInstance(instance, prefix, node);
398 boundInstances[instance] = binder.ErrorList;
399 PopulateValidatorErrorSummary(instance, binder.GetValidationSummary(instance));
402 /// <summary>
403 /// Binds the object of the specified type using the given prefix.
404 /// </summary>
405 /// <typeparam name="T">Target type</typeparam>
406 /// <param name="prefix">The prefix.</param>
407 /// <returns></returns>
408 protected T BindObject<T>(String prefix)
410 return (T) BindObject(typeof(T), prefix);
413 /// <summary>
414 /// Binds the object of the specified type using the given prefix.
415 /// but only using the entries from the collection specified on the <paramref name="from"/>
416 /// </summary>
417 /// <typeparam name="T">Target type</typeparam>
418 /// <param name="from">From.</param>
419 /// <param name="prefix">The prefix.</param>
420 /// <returns></returns>
421 protected T BindObject<T>(ParamStore from, String prefix)
423 return (T) BindObject(from, typeof(T), prefix);
426 /// <summary>
427 /// Binds the object of the specified type using the given prefix.
428 /// but only using the entries from the collection specified on the <paramref name="from"/>
429 /// </summary>
430 /// <typeparam name="T"></typeparam>
431 /// <param name="from">From.</param>
432 /// <param name="prefix">The prefix.</param>
433 /// <param name="excludedProperties">The excluded properties.</param>
434 /// <param name="allowedProperties">The allowed properties.</param>
435 /// <returns></returns>
436 protected T BindObject<T>(ParamStore from, String prefix, String excludedProperties, String allowedProperties)
438 return (T) BindObject(from, typeof(T), prefix, excludedProperties, allowedProperties);