//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ 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; /// /// Represents a single view of a ModelDataSource. /// 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)((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(); /// /// Creates a new ModelDataSourceView. /// 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; } } /// /// Indicates that the view can delete rows. /// public override bool CanDelete { get { return (DeleteMethod.Length != 0); } } /// /// Indicates that the view can add new rows. /// public override bool CanInsert { get { return (InsertMethod.Length != 0); } } /// /// Indicates that the view can do server paging. /// We allow paging by default. /// public override bool CanPage { get { return true; } } /// /// Indicates that the view can sort rows. /// We allow sorting by default. /// public override bool CanSort { get { return true; } } /// /// We allow retrieving total row count by default. /// public override bool CanRetrieveTotalRowCount { get { return true; } } /// /// Indicates that the view can update rows. /// public override bool CanUpdate { get { return (UpdateMethod.Length != 0); } } //All the property setters below are internal for unit tests. /// /// The Data Type Name for the Data Bound Control. /// public string ModelTypeName { get { return _modelTypeName ?? String.Empty; } internal set { if (_modelTypeName != value) { _modelTypeName = value; OnDataSourceViewChanged(EventArgs.Empty); } } } /// /// Name of the method to execute when Delete() is called. /// public string DeleteMethod { get { return _deleteMethod ?? String.Empty; } internal set { _deleteMethod = value; } } /// /// Name of the method to execute when Insert() is called. /// public string InsertMethod { get { return _insertMethod ?? String.Empty; } internal set { _insertMethod = value; } } /// /// Name of the method to execute when Select() is called. /// public string SelectMethod { get { return _selectMethod ?? String.Empty; } internal set { if (_selectMethod != value) { _selectMethod = value; OnDataSourceViewChanged(EventArgs.Empty); } } } /// /// Name of the method to execute when Update() is called. /// public string UpdateMethod { get { return _updateMethod ?? String.Empty; } internal set { _updateMethod = value; } } /// /// First of the DataKeyNames array of the data bound control if applicable (FormView/ListView/GridView/DetailsView) and present. /// 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; } /// /// Invokes the select method and gets the result. Also handles auto paging and sorting when required. /// /// The DataSourceSelectArguments for the select operation. /// When applicable, this method sets the TotalRowCount out parameter in the arguments. /// /// The return value from the select method. 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); } /// /// Evaluates the select method parameters and also determines the options for processing select result like auto paging and sorting behavior. /// /// The DataSourceSelectArguments for the select operation. /// The 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 . /// /// A with the information required to invoke the select method. 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, we treat T as model type for auto paging and sorting. //If the return type is something like CustomType : IQueryable, 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. // 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)); } /// /// This method performs operations on the select method result like auto paging and sorting if applicable. /// /// The DataSourceSelectArguments for the select operation. /// The to use for processing the select result. /// These options are determined in an earlier call to . /// /// The result after operations like auto paging/sorting are done. /// 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; } /// /// 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. /// /// The return value of select method. /// 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. /// 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 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 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; } } /// /// Invokes the Delete method and gets the result. /// 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; } /// /// Invokes the Insert method and gets the result. /// 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; } /// /// Invokes the Update method and gets the result. /// 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; } /// /// This method is used by ExecuteInsert/Update/Delete methods to return the result if it's an integer or return a default value. /// /// The return value from one of the above operations. /// Returns the result as is if it's integer. Otherwise returns -1. 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 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 GetSelectAsyncFunc(DataSourceSelectArguments arguments, DataSourceViewSelectCallback callback, ModelDataSourceMethod method) { Func 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 asyncViewOperation, DataSourceViewOperationCallback callback) { ValidateAsyncModelBindingRequirements(); Func 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; 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 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); } } /// /// Evaluates the method parameters using model binding. /// /// The datasource operation for which parameters are being evaluated. /// The ModelDataSourceMethod object for which the Parameter collection is being evaluated. The MethodInfo property should already be set on this object. /// The values from the data bound control. protected virtual void EvaluateMethodParameters(DataSourceOperation dataSourceOperation, ModelDataSourceMethod modelDataSourceMethod, IDictionary controlValues) { EvaluateMethodParameters(dataSourceOperation, modelDataSourceMethod, controlValues, isPageLoadComplete: false); } /// /// Evaluates the method parameters using model binding. /// /// The datasource operation for which parameters are being evaluated. /// The ModelDataSourceMethod object for which the Parameter collection is being evaluated. The MethodInfo property should already be set on this object. /// The values from the data bound control. /// 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. 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(); if (dataControl != _owner.DataControl) { previousDataControl = dataControl; } } //This is used by ControlValueProvider later. modelBindingExecutionContext.PublishService(_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(previousDataControl); } } private static IValueProvider GetValueProviderFromDictionary(IDictionary controlValues) { Dictionary genericDictionary = new Dictionary(); 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(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; } /// /// 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. /// /// Name of the data method. /// /// A ModelDataSourceMethod with the Instance and MethodInfo set. /// The Parameters collection on ModelDataSourceMethod is still empty after this method. /// 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)); } } } /// /// Extracts the values of all output (out and ref) parameters given a list of parameters and their respective values. /// 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; } /// /// Invokes the data method in a secure fashion. /// /// /// 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. /// /// /// A ModelDataSouceResult object containing the ReturnValue of the method and any out parameters. /// 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); } } /// /// Merges new values in the source dictionary with old values in the destination dictionary. /// 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 } }