Главная страница   /   10.2. Использование специализированных провайдеров значений (ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

Джеффри Палермо

10.2. Использование специализированных провайдеров значений

В ASP.NET MVC 1.0 каждый механизм связывания сам проверял различные источники значений для связывания. Это означало, что если бы мы хотели предоставить новый источник значений, кроме переменных формы, нам бы пришлось переопределить большую часть механизма связывания по умолчанию. Если у нас была модель со смешанными источниками – объект Session, конфигурация, файлы и так далее, было бы нелегко изменить механизм связывания по умолчанию так, чтобы связать разные источники. Механизм связывания по умолчанию в ASP.NET MVC связывает параметры действия контроллера из множества переменных запроса. Мы часто видим, что код в действии контроллера строит модель из множества источников, а их содержание передает в действие контроллера ASP.NET MVC.

Используя дополнительные пользовательские провайдеры значений, которые были представлены в ASP.NET MVC 2, мы можем исключить код поиска из наших действий контроллера, как показано здесь.

// До
public ViewResult LogOnWidget(LogOnWidgetModel model)
{
	bool isAuthenticated = Request.IsAuthenticated;
	model.IsAuthenticated = isAuthenticated;
	model.CurrentUser = Session[""];
	return View(model);
}
// После
public ViewResult LogOnWidget(LogOnWidgetModel model)
{
	bool isAuthenticated = Request.IsAuthenticated;
	model.IsAuthenticated = isAuthenticated;
	return View(model);
}

С ASP.NET MVC 2 и 3 концепция предоставления значений механизму связывания абстрагируется в интерфейсе IValueProvider:

public interface IValueProvider {
	bool ContainsPrefix(string prefix);
	ValueProviderResult GetValue(string key);
}

Сам DefaultModelBinder использует IValueProvider, чтобы создать ValueProviderResult. Затем он использует ValueProviderResult для получения значений, которые используются для связывания наших сложных моделей. Чтобы создать новый пользовательский провайдер значений, мы должны реализовать два ключевых интерфейса. Первый - это IValueProvider, второй является реализацией ValueProviderFactory, которая позволит MVC Framework создать наш пользовательский провайдер значений.

MVC Framework поставляется с несколькими встроенными провайдерами значений, которые идут в комплекте в классе ValueProviderFactories, как показано в листинге.

Листинг 10-3: Класс ValueProviderFactories
public static class ValueProviderFactories
{
	private static readonly ValueProviderFactoryCollection _factories =
		new ValueProviderFactoryCollection() {
			new FormValueProviderFactory(),
			new RouteDataValueProviderFactory(),
			new QueryStringValueProviderFactory(),
			new HttpFileCollectionValueProviderFactory()
		};
	public static ValueProviderFactoryCollection Factories
	{
		get
		{
			return _factories;
		}
	}
}

В листинге 10-3 мы можем видеть, что исходные провайдеры значений включают в себя реализации, которые поддерживают связывание из значений передачи формы, значений маршрута, строк запроса и коллекции файлов. Но мы хотели бы добавить новый провайдер значений для связывания значений из Session, что поможет нам исключить ручной поиск кода из наших контроллеров.

Чтобы добавить новый провайдер значений, нам просто нужно добавить фабрику пользовательского провайдера значений в коллекцию ValueProviderFactories.Factories, как правило, в точке запуска приложения, где мы можем также настроить области, маршруты и так далее, как показано здесь.

protected void Application_Start()
{
	AreaRegistration.RegisterAllAreas();
	ValueProviderFactories.Factories.Add(new SessionValueProviderFactory());
	RegisterRoutes(RouteTable.Routes);
}

ASP.NET MVC требует создать объект-фабрику для обеспечения пользовательского провайдера значений, а не добавлять его напрямую. Для каждого запроса механизм связывания по умолчанию строит всю коллекцию провайдеров значений из зарегистрированных фабрик провайдеров значений.

SessionValueProviderFactory становится достаточно простым, как показано здесь.

Листинг 10-4: Класс SessionValueProviderFactory
public class SessionValueProviderFactory : ValueProviderFactory
{
	public override IValueProvider GetValueProvider(ControllerContext controllerContext)
	{
		return new SessionValueProvider(controllerContext.HttpContext.Session);
	}
}

Мы создаем нашу пользовательскую фабрику провайдеров значений путем наследования от ValueProviderFactory и переопределения метода GetValueProvider. Для каждого запроса будет создаваться экземпляр пользовательского SessionValueProvider путем передачи объекта Session текущего запроса. Вот конструктор:

public class SessionValueProvider : IValueProvider
{
	public SessionValueProvider(HttpSessionStateBase session)
	{
		AddValues(session);
	}
}

Когда создан экземпляр SessionValueProvider с текущим Session, мы хотим изучить Session и кэшировать возможные результаты. В следующем листинге мы кэшируем полученные из Session префиксы и значения для последующего установления соответствий.

Листинг 10-5: Кэш локальных значений и метод AddValues
private readonly HashSet<string> _prefixes = 
	new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, ValueProviderResult> _values = 
	new Dictionary<string, ValueProviderResult>(StringComparer.OrdinalIgnoreCase);

private void AddValues(HttpSessionStateBase session)
{
	if (session.Keys.Count > 0)
	{
		_prefixes.Add("");
	}
	foreach (string key in session.Keys)
	{
		if (key != null)
		{
			_prefixes.Add(key);
			object rawValue = session[key];
			string attemptedValue = session[key].ToString();
			_values[key] = new ValueProviderResult(
				rawValue,
				attemptedValue,
				CultureInfo.CurrentCulture);
		}
	}
}

Строка 8: Убеждается, что сессия не пустая

Строка 10: Регистрирует пустой префикс

Строка 12: Проходит циклом по содержанию сессии

Строка 16: Сохраняет ключи сессии

Строки 19-22: Создает ValueProviderResult

В листинге 10-5 мы сначала проверяем, содержит ли наш объект Session какие-нибудь ключи. Если это так, мы зарегистрируем пустой префикс для установления соответствия. Далее мы проходим циклом по всем ключам в Session, добавляя каждый ключ как доступный префикс для установления соответствия с коллекцией _prefixes. После этого мы извлекаем из Session каждое значение и создаем новый объект ValueProviderResult для каждой пары ключ – значение, найденной в Session. Каждый ValueProviderResult затем добавляется к нашему локальному словарю _values.

Так как после создания экземпляра SessionValueProvider мы находим все возможные результаты провайдеров префиксов и значений, реализация двух других необходимых методов IValueProvider будет очень простой.

Листинг 10-6 : Методы ContainsPrefix и GetValue
public bool ContainsPrefix(string prefix)
{
	return _prefixes.Contains(prefix);
}
public ValueProviderResult GetValue(string key)
{
	ValueProviderResult result;
	_values.TryGetValue(key, out result);
	return result;
}

В методе ContainsPrefix мы возвращаем булево значение, означающее, что наш IValueProvider может установить соответствие с указанным префиксом. Это просто поиск в ранее созданном HashSet-наборе ключей, найденных в объекте Session текущего запроса. Если ContainsPrefix возвращает true, DefaultModelBinder выберет наш провайдер значений для предоставления результата в метод GetValue. Опять же, так как ранее мы уже создали все возможные ValueProviderResults, мы можем просто вернуть кэшированный результат.

Так как же нам воспользоваться пользовательским SessionValueProvider? Мы уже зарегистрировали SessionValueProviderFactory. Далее нам необходим код для использования Session. Из шаблона проекта по умолчанию вы уже знакомы с AccountController. В действие LogOn контроллера AccountController мы добавляем код, чтобы включить профиль вошедшего пользователя Profile в Session, как показано в следующем листинге. Результат этого показан на рисунке 10-3.

Листинг 10-7: Добавление профиля текущего пользователя Profile к Session
var profile = _profileRepository.Find(model.UserName);
if (profile == null)
{
	profile = new Profile(model.UserName);
	_profileRepository.Add(profile);
}
Session[CurrentUserKey] = profile;
FormsService.SignIn(model.UserName, rememberMe);

Мы находим Profile и сохраняем его в Session, чтобы поставщик значений мог его найти. CurrentUserKey – это локальная постоянная в классе AccountController, который показан далее.

[HandleError]
public class AccountController : Controller
{
	public const string CurrentUserKey = "CurrentUser";
...

Как вы помните, SessionValueProvider предоставляет значения для элементов, которые соответствуют любому из значений ключа Session. В нашем случае, для текущего Profile нам нужно только назначить элементу имя CurrentUser и тип Profile, и DefaultModelBinder свяжет наше значение путем извлечения экземпляра Profile из Session. К примеру, мы могли бы создать дочернее действие, которое показывает текущего пользователя, когда он залогинится:

[ChildActionOnly]
public ViewResult LogOnWidget(LogOnWidgetModel model)
{
	bool isAuthenticated = Request.IsAuthenticated;
	model.IsAuthenticated = isAuthenticated;
	return View(model);
}

Ранее нам понадобилось бы извлечь объект Profile непосредственно из Session или загрузить его из другого постоянного хранилища. Но теперь мы можем изменить LogOnWidgetModel и включить элемент CurrentUser, как показано здесь.

public class LogOnWidgetModel
{
	public bool IsAuthenticated { get; set; }
	public Profile CurrentUser { get; set; }
}

Поскольку имя элемента CurrentUser совпадает с нашим ключом Session, SessionValueProvider извлечет Profile из Session и передаст его в DefaultModelBinder, который в конечном итоге передаст это значение в свойство CurrentUser. Виджет входа в систему теперь не обращается к базе данных, как показано на рисунке 10-3.

Рисунок 10-3: Виджет входа извлекает информацию о профиле прямо из Session

Если имя совпадает с ключом Session, значение будет заполнено соответствующим образом. Значения для связывания данных не ограничены строго значениями форм или маршрутов. Мы можем связать данные из любых других источников.

И последнее замечание: провайдеры значений оцениваются в том порядке, в котором они добавляются в коллекцию ValueProviderFactories.Factories. В нашем примере SessionValueProviderFactory был добавлен после стандартных, встроенных фабрик провайдеров значений. Это значит, что если у нас есть значение формы «CurrentUser», оно будет использовано вместо значения Session.