Поставщики данных
88WPF --- Привязка, команды и стили WPF --- Поставщики данных
»» В ДАННОЙ СТАТЬЕ ИСПОЛЬЗУЕТСЯ ИСХОДНЫЙ КОД ДЛЯ ПРИМЕРОВ
В большинстве продемонстрированных до сих пор примеров источник данных верхнего уровня применялся за счет программной установки свойства DataContext простого элемента или свойства ItemsSource списочного элемента. Вообще это наиболее гибкий подход, особенно, если объект данных конструируется другим классом (таким как сущностный класс CarTable). Однако доступны и другие варианты.
Один из возможных приемов состоит в определении объекта данных как ресурса окна (или другого контейнера). Это хорошо работает, если есть возможность сконструировать объект декларативно, но имеет гораздо меньше смысла, если во время выполнения нужно подключаться к внешнему хранилищу данных (подобному базе данных).
Поставщики данных дают возможность привязаться непосредственно к объекту, который определен в разделе ресурсов разметки. Однако вместо привязки к самому объекту данных осуществляется привязка к поставщику данных, который может извлечь или сконструировать этот объект. Такой подход имеет смысл в случае полноценного поставщика данных, обладающего способностью инициировать события в ответ на исключения и предоставляющего свойства, которые позволяют конфигурировать другие детали операции.
К сожалению, поставщики данных, которые включены в WPF, пока не отвечают этому стандарту. Они слишком ограничены, чтобы возиться с ними в ситуациях с внешними данными (например, при извлечении информации из базы данных или файла). Они могут иметь смысл в более простых сценариях, например, поставщик данных мог бы использоваться для соединения вместе нескольких элементов управления, которые передают входную информацию в класс, вычисляющий результат. Однако в данной ситуации от них относительно мало толку, помимо возможности сократить код обработки событий в пользу кода разметки.
Все поставщики данных унаследованы от класса System.Windows.Data.DataSourceProvider. В настоящее время WPF предлагает только два поставщика данных:
- ObjectDataProvider
Получает информацию, вызывая метод другого класса.
- XmlDataProvider
Получает информацию непосредственно из файла XML.
Целью обоих этих объектов является возможность создать экземпляр объекта данных в XAML-разметке, не прибегая к коду обработки событий.
ObjectDataProvider
ObjectDataProvider позволяет получать информацию из другого класса в приложении. Он добавляет следующие средства:
Может создать необходимый объект и передать параметры конструктору.
Может вызвать метод на этом объекте и передать ему параметры метода.
Может создать объект данных асинхронно. (Другими словами, он может подождать, пока будет создано окно, и затем выполнить свою работу в фоновом режиме.)
Например, вот как выглядит базовый поставщик ObjectDataProvider, который создает экземпляр класса CarTable, вызывает его метод GetCars(), который потребуется добавить в этот сущностный класс и делает данные доступными остальной части окна:
<Window.Resources>
<ObjectDataProvider x:Key="CarsListProvider" ObjectType="{x:Type myns:CarTable}" MethodName="GetCars"/>
</Window.Resources>
// В классе CarTable (используем LINQ to Entities)
public static ObservableCollection<CarTable> GetCars()
{
AutoShopEntities context = new AutoShopEntities();
return new ObservableCollection<CarTable>(
context.CarTables.Select(p => p).ToList<CarTable>());
}
Теперь можно создать привязку, которая получает источник из ObjectDataProvider:
<ListBox Name="lstCars" Margin="5" ItemsSource="{Binding Source={StaticResource CarsListProvider}}"/>
Этот дескриптор выглядит так, будто привязывается к ObjectDataProvider, но ObjectDataProvider достаточно интеллектуален, чтобы понять, что на самом деле требуется привязка к списку машин, возвращаемому методом GetCars().
Как и все поставщики данных, ObjectDataProvider предназначен для извлечения данных, но не обновления их. Другими словами, нет способа заставить ObjectDataProvider вызвать другой метод класса CarTable, чтобы инициировать обновление. Это лишь один пример недостаточной зрелости классов поставщиков данных WPF по сравнению с другими реализациями в других платформах, таких как элементы управления источниками данных в ASP.NET.
Обработка ошибок
В нынешнем виде рассматриваемый пример имеет огромный недостаток. Анализатор XAML создает окно и вызывает метод GetCars(), чтобы установить привязку. Все идет гладко, если метод GetCars() возвращает нужные данные, но результат не так хорош, если возникает необработанное исключение (например, база данных слишком занята или недоступна). В этот момент исключение распространяется пузырьком вверх от вызова InitializeComponent() в конструкторе окна. Код, отображающий окно, должен перехватить эту ошибку, что концептуально запутано. И не существует способа продолжить работу и показать окно — даже если перехватить исключение в конструкторе, остальная часть окна не будет инициализирована правильно.
К сожалению, нет простого способа решить эту проблему. Класс ObjectDataProvider включает свойство IsInitialLoadEnabled, которое можно установить в false, чтобы предотвратить вызов GetCars() при первоначальном создании окна. Тогда можно будет вызвать Refresh() позднее, чтобы инициировать вызов. Однако при использовании такого подхода выражение привязки даст сбой, потому что список не сможет извлечь свой источник данных. (Это отличается от большинства ошибок привязки данных, которые молча проглатываются, не инициируя исключений.)
Так каково же решение? Можно сконструировать ObjectDataProvider программно, хотя при этом теряются преимущества от декларативной привязки, что вероятно является главной причиной применения ObjectDataProvider. Другое решение состоит в конфигурировании ObjectDataProvider для асинхронного выполнения работы, как описано в следующем разделе. В этой ситуации исключения вызывают молчаливый сбой (хотя трассировочное сообщение все равно будет выдано в окно Debug (Отладка) с детальным описанием ошибки).
Асинхронная поддержка
Большинство разработчиков сочтут, что для применения ObjectDataProvider есть совсем немного причин. Обычно легче просто привязаться непосредственно к объекту данных и добавить код, который вызовет класс, опрашивающий данные (такой как CarTable). Однако есть одна причина, по которой предпочтение отдается ObjectDataProvider — для получения преимуществ асинхронного запроса данных:
<ObjectDataProvider IsAsynchronous="True" ...
Разметка обманчиво проста. До тех пор, пока свойство ObjectDataProvider.IsAsynchronous установлено в true, поставщик ObjectDataProvider выполняет свою работу в фоновом потоке. В это время интерфейс ничем не связан. Как только объект данных сконструирован и возвращен из метода, ObjectDataProvider делает его доступным для всех привязанных элементов.
Даже не используя ObjectDataProvider, все равно можно запустить код доступа к данным асинхронно. Трюк состоит в применении поддержки многопоточных приложений в WPF. В этом поможет компонент BackgroundWorker. При использовании BackgroundWorker появляется дополнительная поддержка отмены и отображения хода работ. Тем не менее, добавление BackgroundWorker в пользовательский интерфейс требует больше работы, чем простая установка свойства ObjectDataProvider.IsAsynchronous.
XmlDataProvider
Поставщик XmlDataProvider предлагает быстрый и простой способ извлечения XML-данных из отдельного файла, местоположения в Интернете или ресурса приложения, а также обеспечения их доступности элементам приложения.
XmlDataProvider спроектирован как доступный только для чтения (другими словами, он не предусматривает возможности фиксировать изменения) и не умеет работать с XML-данными, полученными из других ресурсов (таких как запись базы данных, сообщение веб-службы и т.п.). В результате XmlDataProvider является исключительно специфическим средством.
Чтобы использовать XmlDataProvider, он должен быть определен и ориентирован на нужный файл за счет установки свойства Source:
<XmlDataProvider x:Key="CarListXmlProvider" XPath="/Cars" Source="XmlDataProviderFile.xml" />
Можно также установить свойство Source программно (что важно, если имя файла заранее не известно). По умолчанию XmlDataProvider загружает XML-содержимое асинхронно, если только явно не установить свойство XmlDataProvider.IsAsynchronous в false.
Ниже приведен фрагмент простого XML-файла, который используется в рассматриваемом примере. Он содержит в себе целый документ в элементе Cars верхнего уровня и помещает каждую машину в отдельный элемент Car (полный код вы можете найти в исходном коде для примеров):
<?xml version="1.0" encoding="utf-8"?>
<Cars>
<Car>
<ID>1</ID>
<CategoryID>2</CategoryID>
<CategoryName>Средний-класс</CategoryName>
<ModelName>Alfa Romeo</ModelName>
<ModelNumber>145</ModelNumber>
<Cost>230000</Cost>
<Description>В 2000 году выпущена спецсерия Alfa Romeo 145 – «Edizione»...</Description>
<ImageCar>images/alfaromeo-145.jpg</ImageCar>
</Car>
<Car>
<ID>2</ID>
<CategoryID>3</CategoryID>
<CategoryName>Бизнес-класс</CategoryName>
<ModelName>Alfa Romeo</ModelName>
<ModelNumber>Spider</ModelNumber>
<Cost>650000</Cost>
<Description>В серийное оснащение всех Alfa Romeo Spider ...</Description>
<ImageCar>images/alfaromeo-spider.jpg</ImageCar>
</Car>
...
Для извлечения информации из XML-разметки используются расширения XPath — мощного стандарта, позволяющего извлекать только интересующие части документа. XPath использует нотацию путей файловой системы. Например, путь / идентифицирует корень XML-документа, a /Cars — корневой элемент по имени <Cars>.
При работе с XmlDataProvider используется свойство Binding.XPath вместо свойства Binding.Path. Это позволяет погружаться в XML-разметку настолько глубоко, насколько нужно.
...
<TextBox Margin="5" Grid.Column="1" Text="{Binding XPath=ModelName}"></TextBox>
<TextBlock Margin="7" Grid.Row="1">Модель:</TextBlock>
<TextBox Margin="5" Grid.Row="1" Grid.Column="1" Text="{Binding XPath=ModelNumber}"></TextBox>
...
После внесения этих изменений получается пример на основе XML, который почти идентичен тому, что использует привязку к базе данных через Entity Framework, который рассматривался до сих пор. Единственное отличие в том, что все данные трактуются как обычный текст. Чтобы преобразовать их в другой тип данных или в другое представление, потребуется конвертер значений.