e79aa3c0ed
Former-commit-id: a2155e9bd80020e49e72e86c44da02a8ac0e57a4
1335 lines
63 KiB
C#
1335 lines
63 KiB
C#
//------------------------------------------------------------------------------
|
|
// <copyright file="ModelDataSourceView.cs" company="Microsoft">
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// </copyright>
|
|
//------------------------------------------------------------------------------
|
|
|
|
namespace System.Web.UI.WebControls {
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Linq.Expressions;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Web.Compilation;
|
|
using System.Web.ModelBinding;
|
|
using System.Web.UI;
|
|
using System.Web.Util;
|
|
|
|
/// <summary>
|
|
/// Represents a single view of a ModelDataSource.
|
|
/// </summary>
|
|
public class ModelDataSourceView : DataSourceView, IStateManager {
|
|
|
|
// Having the immediate caller of MethodInfo.Invoke be a dynamic method gives us two security advantages:
|
|
// - It forces the callee to be a public method on a public type.
|
|
// - It forces a CAS transparency check on the callee.
|
|
private delegate object MethodInvokerDispatcher(MethodInfo methodInfo, object instance, object[] args);
|
|
private static readonly MethodInvokerDispatcher _methodInvokerDispatcher = ((Expression<MethodInvokerDispatcher>)((methodInfo, instance, args) => methodInfo.Invoke(instance, args))).Compile();
|
|
|
|
private ModelDataSource _owner;
|
|
private MethodParametersDictionary _selectParameters;
|
|
private bool _tracking;
|
|
|
|
private string _modelTypeName;
|
|
private string _deleteMethod;
|
|
private string _insertMethod;
|
|
private string _selectMethod;
|
|
private string _updateMethod;
|
|
private string _dataKeyName;
|
|
|
|
private Task _viewOperationTask = null;
|
|
|
|
private const string TotalRowCountParameterName = "totalRowCount";
|
|
private const string MaximumRowsParameterName = "maximumRows";
|
|
private const string StartRowIndexParameterName = "startRowIndex";
|
|
private const string SortParameterName = "sortByExpression";
|
|
|
|
private static readonly object EventCallingDataMethods = new object();
|
|
|
|
/// <summary>
|
|
/// Creates a new ModelDataSourceView.
|
|
/// </summary>
|
|
public ModelDataSourceView(ModelDataSource owner)
|
|
: base(owner, ModelDataSource.DefaultViewName) {
|
|
|
|
if (owner == null) {
|
|
throw new ArgumentNullException("owner");
|
|
}
|
|
|
|
_owner = owner;
|
|
|
|
if (owner.DataControl.Page != null) {
|
|
owner.DataControl.Page.LoadComplete += OnPageLoadComplete;
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Indicates that the view can delete rows.
|
|
/// </summary>
|
|
public override bool CanDelete {
|
|
get {
|
|
return (DeleteMethod.Length != 0);
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Indicates that the view can add new rows.
|
|
/// </summary>
|
|
public override bool CanInsert {
|
|
get {
|
|
return (InsertMethod.Length != 0);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates that the view can do server paging.
|
|
/// We allow paging by default.
|
|
/// </summary>
|
|
public override bool CanPage {
|
|
get {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates that the view can sort rows.
|
|
/// We allow sorting by default.
|
|
/// </summary>
|
|
public override bool CanSort {
|
|
get {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// We allow retrieving total row count by default.
|
|
/// </summary>
|
|
public override bool CanRetrieveTotalRowCount {
|
|
get {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates that the view can update rows.
|
|
/// </summary>
|
|
public override bool CanUpdate {
|
|
get {
|
|
return (UpdateMethod.Length != 0);
|
|
}
|
|
}
|
|
|
|
//All the property setters below are internal for unit tests.
|
|
|
|
/// <summary>
|
|
/// The Data Type Name for the Data Bound Control.
|
|
/// </summary>
|
|
public string ModelTypeName {
|
|
get {
|
|
return _modelTypeName ?? String.Empty;
|
|
}
|
|
internal set {
|
|
if (_modelTypeName != value) {
|
|
_modelTypeName = value;
|
|
OnDataSourceViewChanged(EventArgs.Empty);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Name of the method to execute when Delete() is called.
|
|
/// </summary>
|
|
public string DeleteMethod {
|
|
get {
|
|
return _deleteMethod ?? String.Empty;
|
|
}
|
|
internal set {
|
|
_deleteMethod = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Name of the method to execute when Insert() is called.
|
|
/// </summary>
|
|
public string InsertMethod {
|
|
get {
|
|
return _insertMethod ?? String.Empty;
|
|
}
|
|
internal set {
|
|
_insertMethod = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Name of the method to execute when Select() is called.
|
|
/// </summary>
|
|
public string SelectMethod {
|
|
get {
|
|
return _selectMethod ?? String.Empty;
|
|
}
|
|
internal set {
|
|
if (_selectMethod != value) {
|
|
_selectMethod = value;
|
|
OnDataSourceViewChanged(EventArgs.Empty);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Name of the method to execute when Update() is called.
|
|
/// </summary>
|
|
public string UpdateMethod {
|
|
get {
|
|
return _updateMethod ?? String.Empty;
|
|
}
|
|
internal set {
|
|
_updateMethod = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// First of the DataKeyNames array of the data bound control if applicable (FormView/ListView/GridView/DetailsView) and present.
|
|
/// </summary>
|
|
public string DataKeyName {
|
|
get {
|
|
return _dataKeyName ?? String.Empty;
|
|
}
|
|
internal set {
|
|
if (_dataKeyName != value) {
|
|
_dataKeyName = value;
|
|
OnDataSourceViewChanged(EventArgs.Empty);
|
|
}
|
|
}
|
|
}
|
|
|
|
public event CallingDataMethodsEventHandler CallingDataMethods {
|
|
add {
|
|
Events.AddHandler(EventCallingDataMethods, value);
|
|
}
|
|
remove {
|
|
Events.RemoveHandler(EventCallingDataMethods, value);
|
|
}
|
|
}
|
|
|
|
public void UpdateProperties(string modelTypeName, string selectMethod, string updateMethod, string insertMethod, string deleteMethod, string dataKeyName) {
|
|
ModelTypeName = modelTypeName;
|
|
SelectMethod = selectMethod;
|
|
UpdateMethod = updateMethod;
|
|
InsertMethod = insertMethod;
|
|
DeleteMethod = deleteMethod;
|
|
DataKeyName = dataKeyName;
|
|
}
|
|
|
|
protected virtual void OnCallingDataMethods(CallingDataMethodsEventArgs e) {
|
|
CallingDataMethodsEventHandler handler = Events[EventCallingDataMethods] as CallingDataMethodsEventHandler;
|
|
if (handler != null) {
|
|
handler(_owner.DataControl, e);
|
|
}
|
|
}
|
|
|
|
private void OnPageLoadComplete(object sender, EventArgs e) {
|
|
EvaluateSelectParameters();
|
|
}
|
|
|
|
private static bool IsAutoPagingRequired(MethodInfo selectMethod, bool isReturningQueryable, bool isAsyncSelect) {
|
|
|
|
bool maximumRowsFound = false;
|
|
bool totalRowCountFound = false;
|
|
bool startRowIndexFound = false;
|
|
|
|
foreach (ParameterInfo parameter in selectMethod.GetParameters()) {
|
|
string parameterName = parameter.Name;
|
|
if (String.Equals(StartRowIndexParameterName, parameterName, StringComparison.OrdinalIgnoreCase)) {
|
|
if (parameter.ParameterType.IsAssignableFrom(typeof(int))) {
|
|
startRowIndexFound = true;
|
|
}
|
|
continue;
|
|
}
|
|
if (String.Equals(MaximumRowsParameterName, parameterName, StringComparison.OrdinalIgnoreCase)) {
|
|
if (parameter.ParameterType.IsAssignableFrom(typeof(int))) {
|
|
maximumRowsFound = true;
|
|
}
|
|
continue;
|
|
}
|
|
if (String.Equals(TotalRowCountParameterName, parameterName, StringComparison.OrdinalIgnoreCase)) {
|
|
if (parameter.IsOut && typeof(int).IsAssignableFrom(parameter.ParameterType.GetElementType())) {
|
|
totalRowCountFound = true;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
bool pagingParamsFound;
|
|
if (isAsyncSelect) {
|
|
pagingParamsFound = maximumRowsFound && startRowIndexFound;
|
|
}
|
|
else {
|
|
pagingParamsFound = maximumRowsFound && startRowIndexFound && totalRowCountFound;
|
|
}
|
|
|
|
bool canDoPaging = isReturningQueryable || pagingParamsFound;
|
|
if (!canDoPaging) {
|
|
if (isAsyncSelect) {
|
|
throw new InvalidOperationException(SR.GetString(SR.ModelDataSourceView_InvalidAsyncPagingParameters));
|
|
}
|
|
else {
|
|
throw new InvalidOperationException(SR.GetString(SR.ModelDataSourceView_InvalidPagingParameters));
|
|
}
|
|
}
|
|
|
|
return !pagingParamsFound;
|
|
}
|
|
|
|
private static bool IsAutoSortingRequired(MethodInfo selectMethod, bool isReturningQueryable) {
|
|
|
|
bool sortExpressionFound = false;
|
|
|
|
foreach (ParameterInfo parameter in selectMethod.GetParameters()) {
|
|
string parameterName = parameter.Name;
|
|
if (String.Equals(SortParameterName, parameterName, StringComparison.OrdinalIgnoreCase)) {
|
|
if (parameter.ParameterType.IsAssignableFrom(typeof(string))) {
|
|
sortExpressionFound = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isReturningQueryable && !sortExpressionFound) {
|
|
throw new InvalidOperationException(SR.GetString(SR.ModelDataSourceView_InvalidSortingParameters));
|
|
}
|
|
|
|
return !sortExpressionFound;
|
|
}
|
|
|
|
private object GetPropertyValueByName(object o, string name) {
|
|
var propInfo = o.GetType().GetProperty(name);
|
|
object value = propInfo.GetValue(o, null);
|
|
return value;
|
|
}
|
|
|
|
private void ValidateAsyncModelBindingRequirements() {
|
|
if (!(_owner.DataControl.Page.IsAsync && SynchronizationContextUtil.CurrentMode != SynchronizationContextMode.Legacy)) {
|
|
throw new InvalidOperationException(SR.GetString(SR.ModelDataSourceView_UseAsyncMethodMustBeUsingAsyncPage));
|
|
}
|
|
}
|
|
|
|
private bool RequireAsyncModelBinding(string methodName, out ModelDataSourceMethod method) {
|
|
if (!AppSettings.EnableAsyncModelBinding) {
|
|
method = null;
|
|
return false;
|
|
}
|
|
|
|
method = FindMethod(methodName);
|
|
if (null == method) {
|
|
return false;
|
|
}
|
|
|
|
MethodInfo methodInfo = method.MethodInfo;
|
|
bool returnTypeIsTask = typeof(Task).IsAssignableFrom(methodInfo.ReturnType);
|
|
|
|
return returnTypeIsTask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invokes the select method and gets the result. Also handles auto paging and sorting when required.
|
|
/// </summary>
|
|
/// <param name="arguments">The DataSourceSelectArguments for the select operation.
|
|
/// When applicable, this method sets the TotalRowCount out parameter in the arguments.
|
|
/// </param>
|
|
/// <returns>The return value from the select method.</returns>
|
|
protected virtual object GetSelectMethodResult(DataSourceSelectArguments arguments) {
|
|
|
|
if (SelectMethod.Length == 0) {
|
|
throw new InvalidOperationException(SR.GetString(SR.ModelDataSourceView_SelectNotSupported));
|
|
}
|
|
|
|
DataSourceSelectResultProcessingOptions options = null;
|
|
ModelDataSourceMethod method = EvaluateSelectMethodParameters(arguments, out options);
|
|
ModelDataMethodResult result = InvokeMethod(method);
|
|
return ProcessSelectMethodResult(arguments, options, result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evaluates the select method parameters and also determines the options for processing select result like auto paging and sorting behavior.
|
|
/// </summary>
|
|
/// <param name="arguments">The DataSourceSelectArguments for the select operation.</param>
|
|
/// <param name="selectResultProcessingOptions">The <see cref="System.Web.UI.WebControls.DataSourceSelectResultProcessingOptions"/> to use
|
|
/// for processing the select result once select operation is complete. These options are determined in this method and later used
|
|
/// by the method <see cref="System.Web.UI.WebControls.ModelDataSourceView.ProcessSelectMethodResult"/>.
|
|
/// </param>
|
|
/// <returns>A <see cref="System.Web.UI.WebControls.ModelDataSourceMethod"/> with the information required to invoke the select method.</returns>
|
|
protected virtual ModelDataSourceMethod EvaluateSelectMethodParameters(DataSourceSelectArguments arguments, out DataSourceSelectResultProcessingOptions selectResultProcessingOptions) {
|
|
return EvaluateSelectMethodParameters(arguments, null/*method*/, false /*isAsyncSelect*/, out selectResultProcessingOptions);
|
|
}
|
|
|
|
private ModelDataSourceMethod EvaluateSelectMethodParameters(DataSourceSelectArguments arguments, ModelDataSourceMethod method, bool isAsyncSelect, out DataSourceSelectResultProcessingOptions selectResultProcessingOptions) {
|
|
IOrderedDictionary mergedParameters = MergeSelectParameters(arguments);
|
|
// Resolve the method
|
|
method = method ?? FindMethod(SelectMethod);
|
|
|
|
Type selectMethodReturnType = method.MethodInfo.ReturnType;
|
|
if (isAsyncSelect) {
|
|
selectMethodReturnType = ExtractAsyncSelectReturnType(selectMethodReturnType);
|
|
}
|
|
|
|
Type modelType = ModelType;
|
|
if (modelType == null) {
|
|
//When ModelType is not specified but SelectMethod returns IQueryable<T>, we treat T as model type for auto paging and sorting.
|
|
//If the return type is something like CustomType<U,T> : IQueryable<T>, we should use T for paging and sorting, hence
|
|
//we walk over the return type's generic arguments for a proper match.
|
|
foreach (Type typeParameter in selectMethodReturnType.GetGenericArguments()) {
|
|
if (typeof(IQueryable<>).MakeGenericType(typeParameter).IsAssignableFrom(selectMethodReturnType)) {
|
|
modelType = typeParameter;
|
|
}
|
|
}
|
|
}
|
|
Type queryableModelType = (modelType != null) ? typeof(IQueryable<>).MakeGenericType(modelType) : null;
|
|
|
|
//We do auto paging or auto sorting when the select method is returning an IQueryable and does not have parameters for paging or sorting.
|
|
bool isReturningQueryable = queryableModelType != null && queryableModelType.IsAssignableFrom(selectMethodReturnType);
|
|
|
|
if (isAsyncSelect && isReturningQueryable) {
|
|
// async select method does not support returning IQueryable<>.
|
|
throw new InvalidOperationException(SR.GetString(SR.ModelDataSourceView_InvalidAsyncSelectReturnType, modelType));
|
|
}
|
|
|
|
bool autoPage = false;
|
|
bool autoSort = false;
|
|
|
|
if (arguments.StartRowIndex >= 0 && arguments.MaximumRows > 0) {
|
|
autoPage = IsAutoPagingRequired(method.MethodInfo, isReturningQueryable, isAsyncSelect);
|
|
|
|
if (isAsyncSelect) {
|
|
Debug.Assert(!autoPage, "auto-paging should not be true when using async select method");
|
|
|
|
// custom paging is not supported if the return type is not SelectResult
|
|
if (typeof(SelectResult) != selectMethodReturnType) {
|
|
throw new InvalidOperationException(SR.GetString(SR.ModelDataSourceView_MustUseSelectResultAsReturnType));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!String.IsNullOrEmpty(arguments.SortExpression)) {
|
|
autoSort = IsAutoSortingRequired(method.MethodInfo, isReturningQueryable);
|
|
}
|
|
|
|
selectResultProcessingOptions = new DataSourceSelectResultProcessingOptions() { ModelType = modelType, AutoPage = autoPage, AutoSort = autoSort };
|
|
EvaluateMethodParameters(DataSourceOperation.Select, method, mergedParameters);
|
|
return method;
|
|
}
|
|
|
|
private Type ExtractAsyncSelectReturnType(Type t) {
|
|
// Async select return type is expected to be Task<T>.
|
|
// This method is trying to return type T.
|
|
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Task<>)) {
|
|
Type[] typeArguments = t.GetGenericArguments();
|
|
if (typeArguments.Length == 1) {
|
|
return typeArguments[0];
|
|
}
|
|
}
|
|
|
|
throw new InvalidOperationException(SR.GetString(SR.ModelDataSourceView_InvalidAsyncSelectReturnType, ModelType));
|
|
}
|
|
|
|
/// <summary>
|
|
/// This method performs operations on the select method result like auto paging and sorting if applicable.
|
|
/// </summary>
|
|
/// <param name="arguments">The DataSourceSelectArguments for the select operation.</param>
|
|
/// <param name="selectResultProcessingOptions">The <see cref="System.Web.UI.WebControls.DataSourceSelectResultProcessingOptions"/> to use for processing the select result.
|
|
/// These options are determined in an earlier call to <see cref="System.Web.UI.WebControls.ModelDataSourceView.EvaluateSelectMethodParameters"/>.
|
|
/// </param>
|
|
/// <param name="result">The result after operations like auto paging/sorting are done.</param>
|
|
/// <returns></returns>
|
|
protected virtual object ProcessSelectMethodResult(DataSourceSelectArguments arguments, DataSourceSelectResultProcessingOptions selectResultProcessingOptions, ModelDataMethodResult result) {
|
|
// If the return value is null, there is no more processing to be done
|
|
if (result.ReturnValue == null) {
|
|
return null;
|
|
}
|
|
|
|
bool autoPage = selectResultProcessingOptions.AutoPage;
|
|
bool autoSort = selectResultProcessingOptions.AutoSort;
|
|
Type modelType = selectResultProcessingOptions.ModelType;
|
|
string sortExpression = arguments.SortExpression;
|
|
|
|
if (autoPage) {
|
|
MethodInfo countHelperMethod = typeof(QueryableHelpers).GetMethod("CountHelper").MakeGenericMethod(modelType);
|
|
arguments.TotalRowCount = (int)countHelperMethod.Invoke(null, new object[] { result.ReturnValue });
|
|
|
|
//Bug 180907: We would like to auto sort on DataKeyName when paging is enabled and result is not already sorted by user to overcome a limitation in EF.
|
|
MethodInfo isOrderingMethodFoundMethod = typeof(QueryableHelpers).GetMethod("IsOrderingMethodFound").MakeGenericMethod(modelType);
|
|
bool isOrderingMethodFound = (bool)isOrderingMethodFoundMethod.Invoke(null, new object[] { result.ReturnValue });
|
|
if (!isOrderingMethodFound) {
|
|
if (String.IsNullOrEmpty(sortExpression) && !String.IsNullOrEmpty(DataKeyName)) {
|
|
autoSort = true;
|
|
selectResultProcessingOptions.AutoSort = true;
|
|
sortExpression = DataKeyName;
|
|
}
|
|
}
|
|
}
|
|
else if (arguments.StartRowIndex >= 0 && arguments.MaximumRows > 0) {
|
|
//When paging is handled by developer, we need to set the TotalRowCount parameter from the select method out parameter.
|
|
arguments.TotalRowCount = (int)result.OutputParameters[TotalRowCountParameterName];
|
|
}
|
|
|
|
if (autoPage || autoSort) {
|
|
MethodInfo sortPageHelperMethod = typeof(QueryableHelpers).GetMethod("SortandPageHelper").MakeGenericMethod(modelType);
|
|
object returnValue = sortPageHelperMethod.Invoke(null, new object[] { result.ReturnValue,
|
|
autoPage ? (int?)arguments.StartRowIndex : null,
|
|
autoPage ? (int?)arguments.MaximumRows : null,
|
|
autoSort ? sortExpression : null });
|
|
return returnValue;
|
|
}
|
|
|
|
return result.ReturnValue;
|
|
}
|
|
|
|
private static IOrderedDictionary MergeSelectParameters(DataSourceSelectArguments arguments) {
|
|
bool shouldPage = arguments.StartRowIndex >= 0 && arguments.MaximumRows > 0;
|
|
bool shouldSort = !String.IsNullOrEmpty(arguments.SortExpression);
|
|
// Copy the parameters into a case insensitive dictionary
|
|
IOrderedDictionary mergedParameters = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
|
|
|
|
// Add the sort expression as a parameter if necessary
|
|
if (shouldSort) {
|
|
mergedParameters[SortParameterName] = arguments.SortExpression;
|
|
}
|
|
|
|
// Add the paging arguments as parameters if necessary
|
|
if (shouldPage) {
|
|
// Create a new dictionary with the paging information and merge it in (so we get type conversions)
|
|
IDictionary pagingParameters = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
|
|
pagingParameters[MaximumRowsParameterName] = arguments.MaximumRows;
|
|
pagingParameters[StartRowIndexParameterName] = arguments.StartRowIndex;
|
|
pagingParameters[TotalRowCountParameterName] = 0;
|
|
MergeDictionaries(pagingParameters, mergedParameters);
|
|
}
|
|
|
|
return mergedParameters;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the incoming result after wrapping it into IEnumerable if it's not already one.
|
|
/// Also ensures that the result is properly typed when ModelTypeName property is set.
|
|
/// </summary>
|
|
/// <param name="result">The return value of select method.</param>
|
|
/// <returns>Returns the value wrapping it into IEnumerable if necessary.
|
|
/// If ItemType is set and the return value is not of proper type, throws an InvalidOperationException.
|
|
/// </returns>
|
|
protected virtual IEnumerable CreateSelectResult(object result) {
|
|
return CreateSelectResult(result, false/*isAsyncSelect*/);
|
|
}
|
|
|
|
private IEnumerable CreateSelectResult(object result, bool isAsyncSelect) {
|
|
if (result == null) {
|
|
return null;
|
|
}
|
|
|
|
Type modelType = ModelType;
|
|
|
|
//If it is IEnumerable<ModelType> we return as is.
|
|
Type enumerableModelType = (modelType != null) ? typeof(IEnumerable<>).MakeGenericType(modelType) : typeof(IEnumerable);
|
|
if (enumerableModelType.IsInstanceOfType(result)) {
|
|
return (IEnumerable)result;
|
|
}
|
|
else {
|
|
if (modelType == null || modelType.IsInstanceOfType(result)) {
|
|
//If it is a <ModelType> we wrap it in an array and return.
|
|
return new object[1] { result };
|
|
}
|
|
else {
|
|
//Sorry only the above return types are allowed!!!
|
|
if (isAsyncSelect) {
|
|
throw new InvalidOperationException(SR.GetString(SR.ModelDataSourceView_InvalidAsyncSelectReturnType, modelType));
|
|
}
|
|
else {
|
|
throw new InvalidOperationException(SR.GetString(SR.ModelDataSourceView_InvalidSelectReturnType, modelType));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool IsCancellationRequired(MethodInfo method, out string parameterName) {
|
|
parameterName = null;
|
|
bool cancellationTokenFound = false;
|
|
var lastParameter = method.GetParameters().LastOrDefault();
|
|
if (lastParameter != null && lastParameter.ParameterType == typeof(CancellationToken)) {
|
|
cancellationTokenFound = true;
|
|
parameterName = lastParameter.Name;
|
|
}
|
|
|
|
return cancellationTokenFound;
|
|
}
|
|
|
|
private void SetCancellationTokenIfRequired(ModelDataSourceMethod method, bool isAsyncMethod, CancellationToken? cancellationToken) {
|
|
string cancellationTokenParameterName;
|
|
if (isAsyncMethod && IsCancellationRequired(method.MethodInfo, out cancellationTokenParameterName)) {
|
|
if (null == cancellationToken) {
|
|
throw new InvalidOperationException(SR.GetString(SR.ModelDataSourceView_CancellationTokenIsNotSupported));
|
|
}
|
|
|
|
method.Parameters[cancellationTokenParameterName] = cancellationToken;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invokes the Delete method and gets the result.
|
|
/// </summary>
|
|
protected virtual object GetDeleteMethodResult(IDictionary keys, IDictionary oldValues) {
|
|
return GetDeleteMethodResult(keys, oldValues, null/*method*/, false/*isAsyncMethod*/, null/*cancellationToken*/);
|
|
}
|
|
|
|
private object GetDeleteMethodResult(IDictionary keys, IDictionary oldValues, ModelDataSourceMethod method, bool isAsyncMethod, CancellationToken? cancellationToken) {
|
|
method = method == null ? EvaluateDeleteMethodParameters(keys, oldValues) : EvaluateDeleteMethodParameters(keys, oldValues, method);
|
|
SetCancellationTokenIfRequired(method, isAsyncMethod, cancellationToken);
|
|
ModelDataMethodResult result = InvokeMethod(method, isAsyncMethod);
|
|
|
|
return result.ReturnValue;
|
|
}
|
|
|
|
protected virtual ModelDataSourceMethod EvaluateDeleteMethodParameters(IDictionary keys, IDictionary oldValues) {
|
|
return EvaluateDeleteMethodParameters(keys, oldValues, null/*method*/);
|
|
}
|
|
|
|
private ModelDataSourceMethod EvaluateDeleteMethodParameters(IDictionary keys, IDictionary oldValues, ModelDataSourceMethod method) {
|
|
if (!CanDelete) {
|
|
throw new NotSupportedException(SR.GetString(SR.ModelDataSourceView_DeleteNotSupported));
|
|
}
|
|
|
|
IDictionary caseInsensitiveOldValues = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
|
|
|
|
MergeDictionaries(keys, caseInsensitiveOldValues);
|
|
MergeDictionaries(oldValues, caseInsensitiveOldValues);
|
|
|
|
method = method ?? FindMethod(DeleteMethod);
|
|
EvaluateMethodParameters(DataSourceOperation.Delete, method, caseInsensitiveOldValues);
|
|
return method;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invokes the Insert method and gets the result.
|
|
/// </summary>
|
|
protected virtual object GetInsertMethodResult(IDictionary values) {
|
|
return GetInsertMethodResult(values, null/*method*/, false/*isAsyncMethod&*/, null/*cancellationToken*/);
|
|
}
|
|
|
|
private object GetInsertMethodResult(IDictionary values, ModelDataSourceMethod method, bool isAsyncMethod, CancellationToken? cancellationToken) {
|
|
method = method == null ? EvaluateInsertMethodParameters(values) : EvaluateInsertMethodParameters(values, method);
|
|
SetCancellationTokenIfRequired(method, isAsyncMethod, cancellationToken);
|
|
ModelDataMethodResult result = InvokeMethod(method, isAsyncMethod);
|
|
|
|
return result.ReturnValue;
|
|
}
|
|
|
|
protected virtual ModelDataSourceMethod EvaluateInsertMethodParameters(IDictionary values) {
|
|
return EvaluateInsertMethodParameters(values, null/*method*/);
|
|
}
|
|
|
|
private ModelDataSourceMethod EvaluateInsertMethodParameters(IDictionary values, ModelDataSourceMethod method) {
|
|
if (!CanInsert) {
|
|
throw new NotSupportedException(SR.GetString(SR.ModelDataSourceView_InsertNotSupported));
|
|
}
|
|
|
|
IDictionary caseInsensitiveNewValues = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
|
|
|
|
MergeDictionaries(values, caseInsensitiveNewValues);
|
|
|
|
method = method ?? FindMethod(InsertMethod);
|
|
EvaluateMethodParameters(DataSourceOperation.Insert, method, caseInsensitiveNewValues);
|
|
return method;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invokes the Update method and gets the result.
|
|
/// </summary>
|
|
protected virtual object GetUpdateMethodResult(IDictionary keys, IDictionary values, IDictionary oldValues) {
|
|
return GetUpdateMethodResult(keys, values, oldValues, null/*method*/, false/*isAsyncMethod*/, null/*cancellationToken*/);
|
|
}
|
|
|
|
private object GetUpdateMethodResult(IDictionary keys, IDictionary values, IDictionary oldValues, ModelDataSourceMethod method, bool isAsyncMethod, CancellationToken? cancellationToken) {
|
|
method = method == null ? EvaluateUpdateMethodParameters(keys, values, oldValues) : EvaluateUpdateMethodParameters(keys, values, oldValues, method);
|
|
SetCancellationTokenIfRequired(method, isAsyncMethod, cancellationToken);
|
|
ModelDataMethodResult result = InvokeMethod(method, isAsyncMethod);
|
|
|
|
return result.ReturnValue;
|
|
}
|
|
|
|
protected virtual ModelDataSourceMethod EvaluateUpdateMethodParameters(IDictionary keys, IDictionary values, IDictionary oldValues) {
|
|
return EvaluateUpdateMethodParameters(keys, values, oldValues, null/*method*/);
|
|
}
|
|
|
|
private ModelDataSourceMethod EvaluateUpdateMethodParameters(IDictionary keys, IDictionary values, IDictionary oldValues, ModelDataSourceMethod method) {
|
|
if (!CanUpdate) {
|
|
throw new NotSupportedException(SR.GetString(SR.ModelDataSourceView_UpdateNotSupported));
|
|
}
|
|
|
|
IDictionary caseInsensitiveNewValues = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
|
|
|
|
// We start out with the old values, just to pre-populate the list with items
|
|
// that might not have corresponding new values. For example if a GridView has
|
|
// a read-only field, there will be an old value, but no new value. The data object
|
|
// still has to have *some* value for a given field, so we just use the old value.
|
|
MergeDictionaries(oldValues, caseInsensitiveNewValues);
|
|
MergeDictionaries(keys, caseInsensitiveNewValues);
|
|
MergeDictionaries(values, caseInsensitiveNewValues);
|
|
|
|
method = method ?? FindMethod(UpdateMethod);
|
|
EvaluateMethodParameters(DataSourceOperation.Update, method, caseInsensitiveNewValues);
|
|
return method;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This method is used by ExecuteInsert/Update/Delete methods to return the result if it's an integer or return a default value.
|
|
/// </summary>
|
|
/// <param name="result">The return value from one of the above operations.</param>
|
|
/// <returns>Returns the result as is if it's integer. Otherwise returns -1.</returns>
|
|
private static int GetIntegerReturnValue(object result) {
|
|
return (result is int) ? (int)result : -1 ;
|
|
}
|
|
|
|
// We cannot cache this value since users can change SelectMethod by using UpdateProperties
|
|
// method.
|
|
internal bool IsSelectMethodAsync {
|
|
get {
|
|
ModelDataSourceMethod method;
|
|
return RequireAsyncModelBinding(SelectMethod, out method);
|
|
}
|
|
}
|
|
|
|
public override void Select(DataSourceSelectArguments arguments, DataSourceViewSelectCallback callback) {
|
|
ModelDataSourceMethod method;
|
|
if (RequireAsyncModelBinding(SelectMethod, out method)) {
|
|
// We have to remember the method to make sure the method we later would use in the async function
|
|
// is the method we validate here in case the method later might be changed by UpdateProperties.
|
|
SelectAsync(arguments, callback, method);
|
|
}
|
|
else {
|
|
base.Select(arguments, callback);
|
|
}
|
|
}
|
|
|
|
private void SelectAsync(DataSourceSelectArguments arguments, DataSourceViewSelectCallback callback, ModelDataSourceMethod method) {
|
|
Func<object, Task> func = GetSelectAsyncFunc(arguments, callback, method);
|
|
|
|
var syncContext = _owner.DataControl.Page.Context.SyncContext as AspNetSynchronizationContext;
|
|
if (null == syncContext) {
|
|
throw new InvalidOperationException(SR.GetString(SR.ModelDataSourceView_UseAsyncMethodMustBeUsingAsyncPage));
|
|
}
|
|
|
|
// The first edition of the async model binding feature was implemented by registering async binding
|
|
// function as PageAsyncTask. We, however, decided not to do that because postponing data binding
|
|
// to page async point changed the order of page events and caused many problems.
|
|
// See the comment on SynchronizationHelper.QueueAsynchronousAsync for more details regarding to the PostAsync
|
|
// function.
|
|
syncContext.PostAsync(func, null);
|
|
}
|
|
|
|
private Func<object, Task> GetSelectAsyncFunc(DataSourceSelectArguments arguments, DataSourceViewSelectCallback callback, ModelDataSourceMethod method) {
|
|
Func<object, Task> func = async _ => {
|
|
ValidateAsyncModelBindingRequirements();
|
|
CancellationTokenSource cancellationTokenSource = _owner.DataControl.Page.CreateCancellationTokenFromAsyncTimeout();
|
|
CancellationToken cancellationToken = cancellationTokenSource.Token;
|
|
DataSourceSelectResultProcessingOptions selectResultProcessingOptions = null;
|
|
ModelDataSourceMethod modelMethod = EvaluateSelectMethodParameters(arguments, method, true/*isAsyncSelect*/, out selectResultProcessingOptions);
|
|
SetCancellationTokenIfRequired(modelMethod, true/*isAsyncMethod*/, cancellationToken);
|
|
ModelDataMethodResult result = InvokeMethod(modelMethod);
|
|
IEnumerable finalResult = null;
|
|
if (result.ReturnValue != null) {
|
|
await (Task)result.ReturnValue;
|
|
var returnValue = GetPropertyValueByName(result.ReturnValue, "Result");
|
|
|
|
if (null == returnValue) {
|
|
// do nothing
|
|
}
|
|
// Users needs to use SelectResult as return type to use
|
|
// custom paging.
|
|
else if (returnValue is SelectResult) {
|
|
var viewOperationTask = _viewOperationTask;
|
|
if (viewOperationTask != null) {
|
|
await viewOperationTask;
|
|
}
|
|
|
|
var selectResult = (SelectResult)returnValue;
|
|
arguments.TotalRowCount = selectResult.TotalRowCount;
|
|
finalResult = CreateSelectResult(selectResult.Results, true/*isAsyncSelect*/);
|
|
}
|
|
else {
|
|
// The returnValue does not have to run through ProcessSelectMethodResult() as we
|
|
// don't support auto-paging or auto-sorting when using async select.
|
|
finalResult = CreateSelectResult(returnValue, true/*isAsyncSelect*/);
|
|
}
|
|
}
|
|
|
|
callback(finalResult);
|
|
if (cancellationToken.IsCancellationRequested) {
|
|
throw new TimeoutException(SR.GetString(SR.Async_task_timed_out));
|
|
}
|
|
};
|
|
|
|
return func;
|
|
}
|
|
|
|
public override void Insert(IDictionary values, DataSourceViewOperationCallback callback) {
|
|
if (callback == null) {
|
|
throw new ArgumentNullException("callback");
|
|
}
|
|
|
|
ModelDataSourceMethod method;
|
|
if (RequireAsyncModelBinding(InsertMethod, out method)) {
|
|
ViewOperationAsync((cancellationToken) => (Task)GetInsertMethodResult(values, method, true/*isAsyncMethod*/, cancellationToken), callback);
|
|
}
|
|
else {
|
|
base.Insert(values, callback);
|
|
}
|
|
}
|
|
|
|
public override void Update(IDictionary keys, IDictionary values, IDictionary oldValues, DataSourceViewOperationCallback callback) {
|
|
if (callback == null) {
|
|
throw new ArgumentNullException("callback");
|
|
}
|
|
|
|
ModelDataSourceMethod method;
|
|
if (RequireAsyncModelBinding(UpdateMethod, out method)) {
|
|
ViewOperationAsync((cancellationToken) => (Task)GetUpdateMethodResult(keys, values, oldValues, method, true/*isAsyncMethod*/, cancellationToken), callback);
|
|
}
|
|
else {
|
|
base.Update(keys, values, oldValues, callback);
|
|
}
|
|
}
|
|
|
|
public override void Delete(IDictionary keys, IDictionary oldValues, DataSourceViewOperationCallback callback) {
|
|
if (callback == null) {
|
|
throw new ArgumentNullException("callback");
|
|
}
|
|
|
|
ModelDataSourceMethod method;
|
|
if (RequireAsyncModelBinding(DeleteMethod, out method)) {
|
|
ViewOperationAsync((cancellationToken) => (Task)GetDeleteMethodResult(keys, oldValues, method, true/*isAsyncMethod*/, cancellationToken), callback);
|
|
}
|
|
else {
|
|
base.Delete(keys, oldValues, callback);
|
|
}
|
|
}
|
|
|
|
private void ViewOperationAsync(Func<CancellationToken, Task> asyncViewOperation, DataSourceViewOperationCallback callback) {
|
|
ValidateAsyncModelBindingRequirements();
|
|
|
|
Func<object, Task> func = async _ => {
|
|
CancellationTokenSource cancellationTokenSource = _owner.DataControl.Page.CreateCancellationTokenFromAsyncTimeout();
|
|
CancellationToken cancellationToken = cancellationTokenSource.Token;
|
|
var viewOperationTask = _viewOperationTask;
|
|
if (viewOperationTask != null) {
|
|
await viewOperationTask;
|
|
}
|
|
|
|
var operationTask = asyncViewOperation(cancellationToken);
|
|
_viewOperationTask = operationTask;
|
|
var operationTaskInt = operationTask as Task<int>;
|
|
var operationThrew = false;
|
|
var affectedRecords = -1;
|
|
try {
|
|
if (null != operationTask) {
|
|
await operationTask;
|
|
if (operationTaskInt != null) {
|
|
affectedRecords = operationTaskInt.Result;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex) {
|
|
operationThrew = true;
|
|
if (!callback(affectedRecords, ex)) {
|
|
// Nobody handled the operation error so re-throw
|
|
throw;
|
|
}
|
|
}
|
|
finally {
|
|
_viewOperationTask = null;
|
|
|
|
// Data Method is done executing, turn off the TryUpdateModel
|
|
_owner.DataControl.Page.SetActiveValueProvider(null);
|
|
|
|
if (!operationThrew) {
|
|
if (_owner.DataControl.Page.ModelState.IsValid) {
|
|
OnDataSourceViewChanged(EventArgs.Empty);
|
|
}
|
|
|
|
// Success
|
|
callback(affectedRecords, null);
|
|
}
|
|
}
|
|
|
|
if (cancellationToken.IsCancellationRequested) {
|
|
throw new TimeoutException(SR.GetString(SR.Async_task_timed_out));
|
|
}
|
|
};
|
|
|
|
var syncContext = _owner.DataControl.Page.Context.SyncContext as AspNetSynchronizationContext;
|
|
if (null == syncContext) {
|
|
throw new InvalidOperationException(SR.GetString(SR.ModelDataSourceView_UseAsyncMethodMustBeUsingAsyncPage));
|
|
}
|
|
|
|
syncContext.PostAsync(func, null);
|
|
}
|
|
|
|
protected override int ExecuteDelete(IDictionary keys, IDictionary oldValues) {
|
|
object result = GetDeleteMethodResult(keys, oldValues);
|
|
OnDataSourceViewChanged(EventArgs.Empty);
|
|
return GetIntegerReturnValue(result);
|
|
}
|
|
|
|
protected override int ExecuteInsert(IDictionary values) {
|
|
object result = GetInsertMethodResult(values);
|
|
|
|
Debug.Assert(_owner.DataControl.Page != null);
|
|
//We do not want to databind when ModelState is invaild so that user entered values are not cleared.
|
|
if (_owner.DataControl.Page.ModelState.IsValid) {
|
|
OnDataSourceViewChanged(EventArgs.Empty);
|
|
}
|
|
|
|
return GetIntegerReturnValue(result);
|
|
}
|
|
|
|
protected internal override IEnumerable ExecuteSelect(DataSourceSelectArguments arguments) {
|
|
object result = GetSelectMethodResult(arguments);
|
|
return CreateSelectResult(result);
|
|
}
|
|
|
|
protected override int ExecuteUpdate(IDictionary keys, IDictionary values, IDictionary oldValues) {
|
|
object result = GetUpdateMethodResult(keys, values, oldValues);
|
|
|
|
Debug.Assert(_owner.DataControl.Page != null);
|
|
//We do not want to databind when ModelState is invaild so that user entered values are not cleared.
|
|
if (_owner.DataControl.Page.ModelState.IsValid) {
|
|
OnDataSourceViewChanged(EventArgs.Empty);
|
|
}
|
|
|
|
return GetIntegerReturnValue(result);
|
|
}
|
|
|
|
//For unit testing.
|
|
internal IEnumerable Select(DataSourceSelectArguments arguments) {
|
|
return ExecuteSelect(arguments);
|
|
}
|
|
|
|
//For unit testing.
|
|
internal int Update(IDictionary keys, IDictionary values, IDictionary oldValues) {
|
|
return ExecuteUpdate(keys, values, oldValues);
|
|
}
|
|
|
|
// For unit testing.
|
|
// Return the select async func that we use in the SelectAsync method.
|
|
internal Func<object, Task> SelectAsyncInternal(DataSourceSelectArguments arguments, DataSourceViewSelectCallback callback, ModelDataSourceMethod method) {
|
|
return GetSelectAsyncFunc(arguments, callback, method);
|
|
}
|
|
|
|
//Evaluates the select method parameters using the custom value provides. This is done after page load so that
|
|
//we raise the DataSourceChanged event if the values of parameters change.
|
|
private void EvaluateSelectParameters() {
|
|
if (!String.IsNullOrEmpty(SelectMethod)) {
|
|
ModelDataSourceMethod method = FindMethod(SelectMethod);
|
|
EvaluateMethodParameters(DataSourceOperation.Select, method, controlValues: null, isPageLoadComplete: true);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evaluates the method parameters using model binding.
|
|
/// </summary>
|
|
/// <param name="dataSourceOperation">The datasource operation for which parameters are being evaluated.</param>
|
|
/// <param name="modelDataSourceMethod">The ModelDataSourceMethod object for which the Parameter collection is being evaluated. The MethodInfo property should already be set on this object.</param>
|
|
/// <param name="controlValues">The values from the data bound control.</param>
|
|
protected virtual void EvaluateMethodParameters(DataSourceOperation dataSourceOperation, ModelDataSourceMethod modelDataSourceMethod, IDictionary controlValues) {
|
|
EvaluateMethodParameters(dataSourceOperation, modelDataSourceMethod, controlValues, isPageLoadComplete: false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evaluates the method parameters using model binding.
|
|
/// </summary>
|
|
/// <param name="dataSourceOperation">The datasource operation for which parameters are being evaluated.</param>
|
|
/// <param name="modelDataSourceMethod">The ModelDataSourceMethod object for which the Parameter collection is being evaluated. The MethodInfo property should already be set on this object.</param>
|
|
/// <param name="controlValues">The values from the data bound control.</param>
|
|
/// <param name="isPageLoadComplete">This must be set to true only when this method is called in Page's LoadComplete event handler
|
|
/// to evaluate the select method parameters that use custom value providers so that we can identify any changes
|
|
/// to those and mark the data-bound control for data binding if necessary.</param>
|
|
protected virtual void EvaluateMethodParameters(DataSourceOperation dataSourceOperation, ModelDataSourceMethod modelDataSourceMethod, IDictionary controlValues, bool isPageLoadComplete) {
|
|
|
|
Debug.Assert(_owner.DataControl.Page != null);
|
|
Debug.Assert(_owner.DataControl.TemplateControl != null);
|
|
|
|
MethodInfo actionMethod = modelDataSourceMethod.MethodInfo;
|
|
|
|
IModelBinder binder = ModelBinders.Binders.DefaultBinder;
|
|
|
|
IValueProvider dataBoundControlValueProvider = GetValueProviderFromDictionary(controlValues);
|
|
|
|
ModelBindingExecutionContext modelBindingExecutionContext = _owner.DataControl.Page.ModelBindingExecutionContext;
|
|
|
|
Control previousDataControl = null;
|
|
if (BinaryCompatibility.Current.TargetsAtLeastFramework46) {
|
|
// DevDiv 1087698: a child control overwrites its parent controls's modelBindingExecutionContext,
|
|
// which may cause a problem for the parent control to find control by controlId.
|
|
Control dataControl = modelBindingExecutionContext.TryGetService<Control>();
|
|
if (dataControl != _owner.DataControl) {
|
|
previousDataControl = dataControl;
|
|
}
|
|
}
|
|
|
|
//This is used by ControlValueProvider later.
|
|
modelBindingExecutionContext.PublishService<Control>(_owner.DataControl);
|
|
|
|
//This is done for the TryUpdateModel to work inside a Data Method.
|
|
if (dataSourceOperation != DataSourceOperation.Select) {
|
|
_owner.DataControl.Page.SetActiveValueProvider(dataBoundControlValueProvider);
|
|
}
|
|
|
|
var methodParameters = actionMethod.GetParameters();
|
|
ParameterInfo lastParameter = null;
|
|
if (methodParameters.Length > 0) {
|
|
lastParameter = methodParameters[methodParameters.Length - 1];
|
|
}
|
|
|
|
foreach (ParameterInfo parameterInfo in methodParameters) {
|
|
object value = null;
|
|
string modelName = parameterInfo.Name;
|
|
|
|
if (parameterInfo.ParameterType == typeof(ModelMethodContext)) {
|
|
//ModelMethodContext is a special parameter we pass in for enabling developer to call
|
|
//TryUpdateModel when Select/Update/Delete/InsertMethods are on a custom class.
|
|
value = new ModelMethodContext(_owner.DataControl.Page);
|
|
}
|
|
//Do not attempt model binding the out parameters
|
|
else if (!parameterInfo.IsOut) {
|
|
bool validateRequest;
|
|
IValueProvider customValueProvider = GetCustomValueProvider(modelBindingExecutionContext, parameterInfo, ref modelName, out validateRequest);
|
|
|
|
//When we are evaluating the parameter at the time of page load, we do not want to populate the actual ModelState
|
|
//because there will be another evaluation at data-binding causing duplicate errors if model validation fails.
|
|
ModelStateDictionary modelState = isPageLoadComplete ? new ModelStateDictionary() : _owner.DataControl.Page.ModelState;
|
|
|
|
ModelBindingContext bindingContext = new ModelBindingContext() {
|
|
ModelBinderProviders = ModelBinderProviders.Providers,
|
|
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, parameterInfo.ParameterType),
|
|
ModelState = modelState,
|
|
ModelName = modelName,
|
|
ValueProvider = customValueProvider,
|
|
ValidateRequest = validateRequest
|
|
};
|
|
|
|
//Select parameters that take custom values providers are tracked by ViewState so that
|
|
//we can detect any changes from previous page request and mark the data bound control for data binding if necessary.
|
|
if (dataSourceOperation == DataSourceOperation.Select && customValueProvider != null && parameterInfo.ParameterType.IsSerializable) {
|
|
if (!SelectParameters.ContainsKey(parameterInfo.Name)) {
|
|
SelectParameters.Add(parameterInfo.Name, new MethodParameterValue());
|
|
}
|
|
|
|
if (binder.BindModel(modelBindingExecutionContext, bindingContext)) {
|
|
value = bindingContext.Model;
|
|
}
|
|
SelectParameters[parameterInfo.Name].UpdateValue(value);
|
|
}
|
|
else {
|
|
if (isPageLoadComplete) {
|
|
Debug.Assert(dataSourceOperation == DataSourceOperation.Select, "Only Select Operation should have been done immediately after page load");
|
|
//When this method is called as part of Page's LoadComplete event handler we do not have values in defaultValueProvider
|
|
//(i.e., values from DataBoundControl), so we need not evaluate the parameters values.
|
|
continue;
|
|
}
|
|
|
|
if (customValueProvider == null) {
|
|
bindingContext.ValueProvider = dataBoundControlValueProvider;
|
|
}
|
|
|
|
if (binder.BindModel(modelBindingExecutionContext, bindingContext)) {
|
|
value = bindingContext.Model;
|
|
}
|
|
}
|
|
|
|
// We set the CancellationToken after EvaluateMethodParameters().
|
|
// We don't want to set a null value to a CancellationToken variable.
|
|
if (parameterInfo == lastParameter && typeof(CancellationToken) == parameterInfo.ParameterType && value == null) {
|
|
value = CancellationToken.None;
|
|
}
|
|
|
|
if (!isPageLoadComplete) {
|
|
ValidateParameterValue(parameterInfo, value, actionMethod);
|
|
}
|
|
}
|
|
modelDataSourceMethod.Parameters.Add(parameterInfo.Name, value);
|
|
}
|
|
|
|
if (previousDataControl != null) {
|
|
modelBindingExecutionContext.PublishService<Control>(previousDataControl);
|
|
}
|
|
}
|
|
|
|
private static IValueProvider GetValueProviderFromDictionary(IDictionary controlValues) {
|
|
Dictionary<string, object> genericDictionary = new Dictionary<string, object>();
|
|
|
|
if (controlValues != null) {
|
|
foreach (DictionaryEntry entry in controlValues) {
|
|
Debug.Assert(entry.Key is string, "Some key value is not string");
|
|
genericDictionary.Add((string)entry.Key, entry.Value);
|
|
}
|
|
}
|
|
|
|
return new DictionaryValueProvider<object>(genericDictionary, CultureInfo.CurrentCulture);
|
|
}
|
|
|
|
private IValueProvider GetCustomValueProvider(ModelBindingExecutionContext modelBindingExecutionContext, ParameterInfo parameterInfo, ref string modelName, out bool validateRequest) {
|
|
validateRequest = true;
|
|
object[] valueProviderAttributes = parameterInfo.GetCustomAttributes(typeof(IValueProviderSource), false);
|
|
|
|
if (valueProviderAttributes.Count() > 1) {
|
|
throw new NotSupportedException(SR.GetString(SR.ModelDataSourceView_MultipleValueProvidersNotSupported, parameterInfo.Name));
|
|
}
|
|
|
|
if (valueProviderAttributes.Count() > 0) {
|
|
IValueProviderSource valueProviderAttribute = (IValueProviderSource)valueProviderAttributes[0];
|
|
if (valueProviderAttribute is IModelNameProvider) {
|
|
string name = ((IModelNameProvider)valueProviderAttribute).GetModelName();
|
|
if (!String.IsNullOrEmpty(name)) {
|
|
modelName = name;
|
|
}
|
|
}
|
|
if (valueProviderAttribute is IUnvalidatedValueProviderSource) {
|
|
validateRequest = ((IUnvalidatedValueProviderSource)valueProviderAttribute).ValidateInput;
|
|
}
|
|
return valueProviderAttribute.GetValueProvider(modelBindingExecutionContext);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds the method to be executed. Raises the CallingDataMethods event to see if developer opted in for custom model method look-up instead of page/usercontrol code behind.
|
|
/// Uses the TemplateControl type as a fallback.
|
|
/// </summary>
|
|
/// <param name="methodName">Name of the data method.</param>
|
|
/// <returns>
|
|
/// A ModelDataSourceMethod with the Instance and MethodInfo set.
|
|
/// The Parameters collection on ModelDataSourceMethod is still empty after this method.
|
|
/// </returns>
|
|
protected virtual ModelDataSourceMethod FindMethod(string methodName) {
|
|
CallingDataMethodsEventArgs e = new CallingDataMethodsEventArgs();
|
|
OnCallingDataMethods(e);
|
|
Type type;
|
|
BindingFlags flags;
|
|
object instance;
|
|
|
|
if (e.DataMethodsType != null) {
|
|
if (e.DataMethodsObject != null) {
|
|
throw new InvalidOperationException(SR.GetString(SR.ModelDataSourceView_MultipleModelMethodSources, methodName));
|
|
}
|
|
flags = BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy;
|
|
instance = null;
|
|
type = e.DataMethodsType;
|
|
}
|
|
else if (e.DataMethodsObject != null) {
|
|
flags = BindingFlags.Public | BindingFlags.Instance;
|
|
instance = e.DataMethodsObject;
|
|
type = instance.GetType();
|
|
}
|
|
else {
|
|
//The compiled page code is a child class of code behind class where usually static methods are defined.
|
|
//We will not get those methods unless we use FlattenHierarchy.
|
|
flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy;
|
|
instance = _owner.DataControl.TemplateControl;
|
|
type = instance.GetType();
|
|
}
|
|
|
|
MethodInfo[] allMethods = type.GetMethods(flags);
|
|
MethodInfo[] actionMethods = Array.FindAll(allMethods, methodInfo => methodInfo.Name.Equals(methodName, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (actionMethods.Length != 1) {
|
|
throw new InvalidOperationException(SR.GetString(SR.ModelDataSourceView_DataMethodNotFound, methodName, type));
|
|
}
|
|
|
|
ValidateMethodIsCallable(actionMethods[0]);
|
|
|
|
return new ModelDataSourceMethod(instance: instance, methodInfo: actionMethods[0]);
|
|
}
|
|
|
|
private void ValidateMethodIsCallable(MethodInfo methodInfo) {
|
|
// we can't call methods with open generic type parameters
|
|
if (methodInfo.ContainsGenericParameters) {
|
|
throw new InvalidOperationException(SR.GetString(SR.ModelDataSourceView_CannotCallOpenGenericMethods, methodInfo, methodInfo.ReflectedType.FullName));
|
|
}
|
|
|
|
// we can't call methods with ref parameters
|
|
ParameterInfo[] parameterInfos = methodInfo.GetParameters();
|
|
foreach (ParameterInfo parameterInfo in parameterInfos) {
|
|
if (parameterInfo.ParameterType.IsByRef && !parameterInfo.Name.Equals(TotalRowCountParameterName, StringComparison.OrdinalIgnoreCase)) {
|
|
throw new InvalidOperationException(SR.GetString(SR.ModelDataSourceView_CannotCallMethodsWithOutOrRefParameters,
|
|
methodInfo, methodInfo.ReflectedType.FullName, parameterInfo));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts the values of all output (out and ref) parameters given a list of parameters and their respective values.
|
|
/// </summary>
|
|
private OrderedDictionary GetOutputParameters(ParameterInfo[] parameters, object[] values) {
|
|
OrderedDictionary outputParameters = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
|
|
for (int i = 0; i < parameters.Length; i++) {
|
|
ParameterInfo parameter = parameters[i];
|
|
if (parameter.ParameterType.IsByRef) {
|
|
outputParameters[parameter.Name] = values[i];
|
|
}
|
|
}
|
|
return outputParameters;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invokes the data method in a secure fashion.
|
|
/// </summary>
|
|
/// <param name="method">
|
|
/// The ModelDataSouceMethod object specifying the Instance on which the method should be invoked (null for static methods),
|
|
/// the MethodInfo of the method to be invoked and the Parameters for invoking the method.
|
|
/// All the above properties should be populated before this method is called.
|
|
/// </param>
|
|
/// <returns>
|
|
/// A ModelDataSouceResult object containing the ReturnValue of the method and any out parameters.
|
|
/// </returns>
|
|
protected virtual ModelDataMethodResult InvokeMethod(ModelDataSourceMethod method) {
|
|
return InvokeMethod(method, false/*isAsyncMethod*/);
|
|
}
|
|
|
|
private ModelDataMethodResult InvokeMethod(ModelDataSourceMethod method, bool isAsyncMethod) {
|
|
object returnValue = null;
|
|
|
|
object[] parameterValues = null;
|
|
if (method.Parameters != null && method.Parameters.Count > 0) {
|
|
parameterValues = new object[method.Parameters.Count];
|
|
for (int i = 0; i < method.Parameters.Count; i++) {
|
|
parameterValues[i] = method.Parameters[i];
|
|
}
|
|
}
|
|
|
|
returnValue = _methodInvokerDispatcher(method.MethodInfo, method.Instance, parameterValues);
|
|
OrderedDictionary outputParameters = GetOutputParameters(method.MethodInfo.GetParameters(), parameterValues);
|
|
method.Instance = null;
|
|
|
|
// Data Method is done executing, turn off the TryUpdateModel
|
|
// Do not turn off the TryUpdateModel at this point when the method is async
|
|
if (!isAsyncMethod) {
|
|
_owner.DataControl.Page.SetActiveValueProvider(null);
|
|
}
|
|
|
|
return new ModelDataMethodResult(returnValue, outputParameters);
|
|
}
|
|
|
|
protected virtual bool IsTrackingViewState() {
|
|
return _tracking;
|
|
}
|
|
|
|
protected virtual void LoadViewState(object savedState) {
|
|
if (savedState != null) {
|
|
((IStateManager)SelectParameters).LoadViewState(savedState);
|
|
}
|
|
}
|
|
|
|
protected virtual object SaveViewState() {
|
|
return _selectParameters != null ? ((IStateManager)_selectParameters).SaveViewState() : null;
|
|
}
|
|
|
|
protected virtual void TrackViewState() {
|
|
_tracking = true;
|
|
if (_selectParameters != null) {
|
|
((IStateManager)_selectParameters).TrackViewState();
|
|
}
|
|
}
|
|
|
|
private void ValidateParameterValue(ParameterInfo parameterInfo, object value, MethodInfo methodInfo) {
|
|
if (value == null && !TypeHelpers.TypeAllowsNullValue(parameterInfo.ParameterType)) {
|
|
// tried to pass a null value for a non-nullable parameter type
|
|
string message = String.Format(CultureInfo.CurrentCulture, SR.GetString(SR.ModelDataSourceView_ParameterCannotBeNull),
|
|
parameterInfo.Name, parameterInfo.ParameterType, methodInfo, methodInfo.DeclaringType);
|
|
throw new InvalidOperationException(message);
|
|
}
|
|
|
|
if (value != null && !parameterInfo.ParameterType.IsInstanceOfType(value)) {
|
|
// value was supplied but is not of the proper type
|
|
string message = String.Format(CultureInfo.CurrentCulture, SR.GetString(SR.ModelDataSourceView_ParameterValueHasWrongType),
|
|
parameterInfo.Name, methodInfo, methodInfo.DeclaringType, value.GetType(), parameterInfo.ParameterType);
|
|
throw new InvalidOperationException(message);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Merges new values in the source dictionary with old values in the destination dictionary.
|
|
/// </summary>
|
|
private static void MergeDictionaries(IDictionary source, IDictionary destination) {
|
|
Debug.Assert(destination != null);
|
|
|
|
if (source != null) {
|
|
foreach (DictionaryEntry de in source) {
|
|
object value = de.Value;
|
|
// If the reference collection contains this parameter, we will convert its type to match it
|
|
string parameterName = (string)de.Key;
|
|
destination[parameterName] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
private Type ModelType {
|
|
get {
|
|
string modelTypeName = ModelTypeName;
|
|
|
|
if (String.IsNullOrEmpty(modelTypeName)) {
|
|
return null;
|
|
}
|
|
|
|
// Load the data object type using BuildManager
|
|
return BuildManager.GetType(modelTypeName, true, true);
|
|
}
|
|
}
|
|
|
|
private MethodParametersDictionary SelectParameters {
|
|
get {
|
|
if (_selectParameters == null) {
|
|
_selectParameters = new MethodParametersDictionary();
|
|
|
|
_selectParameters.ParametersChanged += OnSelectParametersChanged;
|
|
|
|
if (_tracking) {
|
|
((IStateManager)_selectParameters).TrackViewState();
|
|
}
|
|
}
|
|
return _selectParameters;
|
|
}
|
|
}
|
|
|
|
private void OnSelectParametersChanged(object sender, EventArgs e) {
|
|
OnDataSourceViewChanged(EventArgs.Empty);
|
|
}
|
|
|
|
#region Implementation of IStateManager
|
|
|
|
bool IStateManager.IsTrackingViewState {
|
|
get {
|
|
return IsTrackingViewState();
|
|
}
|
|
}
|
|
|
|
void IStateManager.LoadViewState(object savedState) {
|
|
LoadViewState(savedState);
|
|
}
|
|
|
|
object IStateManager.SaveViewState() {
|
|
return SaveViewState();
|
|
}
|
|
|
|
void IStateManager.TrackViewState() {
|
|
TrackViewState();
|
|
}
|
|
#endregion
|
|
}
|
|
}
|
|
|