//------------------------------------------------------------------------------
//
// 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