Сейчас редкое приложение обходится без данных, хранящихся в центральном большом хранилище (Store - хранилище), - базе данных (БД). Такие приложения обычно называют управляемые данными. Их можно построить и по технологии WPF. Для взаимодействия с БД традиционно используются возможности объектов ADO.NET, а вот для отображения данных можно применить элементы и механизмы WPF, в том числе - привязку.
Главная трудность, которая нас ожидает при выполнении упражнений данной темы, - вспомнить классы ADO.NET и правильно их использовать. Такая ситуация естественна, пока не накопилось нужного количества практического опыта, а это дело наживное. Тем более, что необходимый для понимания данной темы минимум сведений по ADO.NET был изложен в начале этого курса, и к нему всегда, при необходимости, можно вернуться.
Начнем с привязки списковых элементов WPF к табличным объектам ADO.NET для простого просмотра. Нам нужно будет извлечь данные из БД и заполнить ими некоторый объект, способный автономно хранить добытые данные в оперативной памяти, не занимая при этом соединение с базой данных. Затем следует связать какой-нибудь списковый интерфейсный элемент WPF с этим объектом - источником данных.
В качестве хранилища данных будем использовать учебную базу данных Northwind.mdb типа OLE DB (Object Linking and
Более того, в данном упражнении пока будем работать только с таблицей Employees БД Northwind.mdb (для единообразия). Постепенно перейдем и к другим таблицам, особенно при рассмотрении вопросов работы со взаимосвязанными таблицами БД, находящимися в отношении (Relations) "главная-подробности" (" master-detail ").
Для удобства, еще раз приведем схему, отображающую структуру БД Northwind.mdb, с которой мы ранее уже встречались

Прежде, чем двигаться дальше, уместно напомнить о существовании многоуровневого подхода к проектированию приложений, когда функциональные возможности, компоненты взаимодействия с пользователем и код проекта в целом искусственно делятся на отдельные логические уровни.
Уровней может быть несколько в зависимости от размера проекта, но обычно их бывает четыре. Об этом мы раньше говорили, но здесь еще раз вспомним:
Очень часто границы между уровнями размыты, но все же надо их уметь распознавать или искусственно придумывать по какому-нибудь признаку, чтобы структурировать код приложения. Это дисциплинирует программиста, делает отдельные части кода более независимыми и уменьшает количество возможных ошибок. Старый методологический принцип - разделяй и властвуй (в данном случае - вертикаль власти), так хорошо помогающий нашему, порой еще слабосильному, сознанию.
Перечисленные уровни стремятся изолировать друг от друга, упаковывая код в отдельные файлы, динамические библиотеки, классы и пространства имен. Такой способ позволяет скрыть основную массу деталей и оставить только интерфейсные члены для взаимодействия с другими частями приложения. Подобным же образом намерены поступать и мы (клянемся!).
Командой File/New/Project создайте новое решение DataBindingTable с одноименным проектом
В панели Solution Explorer выделите корень проекта и командой Add/New Folder контекстного меню создайте подкаталог Data
Через контекстное меню панели Solution Explorer скопируйте из прилагаемой папки Source файл Northwind.mdb командой Add/Existing Item (измените фильтр диалогового окна на All Files). При появлении мастера Data Source Configuration Wizard отмените его кнопкой Cancel, поскольку мы пока не собираемся создавать типизированный набор данных
Выделите в панели Solution Explorer файл Northwind.mdb и через панель Properties проверьте, что его свойства имеют значениеBuild Action=ContentCopy to Output Directory=Copy always
В панели Solution Explorer добавьте для корневого узла проекта командой Add/New Item заготовку файла конфигурации приложения с именем App.config
Заполните конфигурационный файл App.config кодом настройки строки соединения с БД, значение которой мы будем извлекать в процедурном коде во время выполнения приложения с помощью класса ConfigurationManager<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="MyNorthwind"
connectionString="Provider=Microsoft.Jet.OLEDB.4.0;
Data Source=|DataDirectory|\Data\Northwind.mdb"
providerName="System.Data.OleDb" />
</connectionStrings>
</configuration>
В панели Solution Explorer вызовите для узла References проекта контекстное меню и командой Add Reference добавьте ссылку на библиотечную сборку System.Configuration.dll, в которой находится нужный нам класс ConfigurationManager для работы с конфигурационным файлом App.config из процедурного кода
Выделите в панели Solution Explorer узел проекта и добавьте к нему командой Project/Add Class новый файл с именем StoreNorthwindDB.cs
Добавьте в начало файла StoreNorthwindDB.cs строку подключения пространств имен инфраструктуры ADO.NET и заполните класс StoreNorthwindDB следующим кодомusing System;
using System.Collections.Generic;
using System.Text;
// Подключение пространств имен инфраструктуры ADO.NET
using System.Data;
using System.Data.OleDb;
using System.Windows;// Для MessageBox
namespace DataBindingTable
{
// Класс для доступа к БД
public class StoreNorthwindDB
{
// Извлекаем в поле строку соединения из файла App.config
String connectionString = System.Configuration.
ConfigurationManager.ConnectionStrings["MyNorthwind"].ConnectionString;
//*********************************************************
// Метод извлечения данных из таблицы Employees
// хранилища базы данных в ADO.NET-объект DataTable
//*********************************************************
DataTable dtEmployees = null;// Ссылка на объект DataTable
public DataTable LoadTableEmployees()
{
// Загрузим таблицу Employees только один раз
if (dtEmployees != null)
return dtEmployees;
// Заполняем объект таблицы Employees данными из БД
dtEmployees = new DataTable();
using (OleDbConnection conn = new OleDbConnection(connectionString))
{
OleDbCommand selectCommand = conn.CreateCommand();
OleDbDataAdapter adapter = new OleDbDataAdapter(selectCommand);
// Загружает данные и схему таблицы Employees
selectCommand.CommandText = "SELECT EmployeeID, " +
"(LastName + ', ' + FirstName) AS FullName, " +
"Address, BirthDate, Region FROM Employees";
try
{
// Метод сам открывает БД и сам же ее закрывает
adapter.Fill(dtEmployees);
}
catch
{
MessageBox.Show("Ошибка подключения к БД");
}
finally
{
conn.Close(); // На всякий случай!
}
}
return dtEmployees;
}
}
}
Инструкция using(), в которой создается соединение с БД, автоматически закроет его после выполнения своего блока кода, хотя то же самое сделает и метод adapter.Fill(). В SQL-запросе к БД наряду с извлечением столбцов оригинальных мы одновременно формируем вычислимый столбец FullName. Метод LoadTableEmployees(), который будет вызываться в клиенте класса, вернет ссылку на объект dtEmployees типа DataTable, загруженный данными таблицы.
Для того, чтобы получить только один экземпляр класса StoreNorthwindDB, и чтобы он был доступен во всех окнах приложения, разместим код его создания в классе App.
Откройте файл App.xaml.cs и добавьте в класс App уровня приложения следующий кодusing System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Windows;
namespace DataBindingTable
{
public partial class App : Application
{
// Базовое поле для свойства
private static StoreNorthwindDB storeNorthwindDB =
new StoreNorthwindDB();
// Свойство для базового поля со ссылкой на экземпляр класса
public static StoreNorthwindDB StoreNorthwindDB
{
get { return storeNorthwindDB; }
}
}
}
Теперь нам нужно создать промежуточный класс, который бы инкапсулировал поля загруженного данными объекта DataTable в общедоступные свойства. К общедоступным свойствам мы будем привязывать интерфейсные элементы пока для просмотра данных. Именно, только для общедоступных свойств привязка к пользовательскому классу будет работать. Вспомогательный класс разместим в отдельном файле.
В панели Solution Explorer для узла проекта DataBindingTable вызовите контекстное меню и командой Add/Class создайте файл Employees1.cs
Заполните файл Employees1.cs такusing System;
using System.Collections.Generic;
using System.Text;
// Подключение пространства имен для DataTable
using System.Data;
namespace DataBindingTable
{
// Класс свойств доступа к данным таблицы Employees
class Employees1
{
//********************************************************
// Cвойства доступа для привязки и базовые поля
//********************************************************
// Для списка надо массив
string[] listFullName = null;
public string[] ListFullName
{
get
{
if (listFullName == null)
{
listFullName = new string[dt.Rows.Count];
int i = 0;
// Перебираем записи и заполняем массив для списка
foreach (DataRow row in dt.Rows)
{
listFullName[i] = (string)dt.Rows[i]["FullName"];
i++;
}
}
return listFullName;
}
}
int employeeID;
public int EmployeeID
{
get { return employeeID; }
}
string fullName;
public string FullName
{
get { return fullName; }
}
string address;
public string Address
{
get { return address; }
}
string birthDate;
public string BirthDate
{
get { return birthDate; }
}
string region;
public string Region
{
get { return region; }
}
// Навигация по таблице ADO.NET
public Employees1 GetEmployee(int ID)
{
// Проверяем границы массива
if (ID < 0 || ID >= dt.Rows.Count)
return null;
// Наполняем свойства полями записи ID
// для последующего их извлечения свойствами
// в привязанные элементы интерфейса.
// Входные параметры приводятся явно
// к тому типу, который ожидает метод
return new Employees1(
(int)dt.Rows[ID]["EmployeeID"],
(string)dt.Rows[ID]["FullName"],
(string)dt.Rows[ID]["Address"],
dt.Rows[ID]["BirthDate"].ToString(),
dt.Rows[ID]["Region"].ToString()
);
}
// Конструктор с параметрами
public Employees1(int employeeID,
string fullName, string address,
string birthDate, string region)
{
this.employeeID = employeeID;
this.fullName = fullName;
this.address = address;
this.birthDate = birthDate;
this.region = region;
}
// Конструктор по умолчанию
public Employees1() { }
// Ссылка на исходную таблицу-объект ADO.NET
DataTable dt = App.StoreNorthwindDB.LoadTableEmployees();
}
}
Обратите внимание, что в классе Employees1 мы предусмотрели свойства доступа только с аксессором get, значит инкапсулированные данные пока можно будет только просматривать и тип привязки интерфейсных элементов-приемников должен быть только OneWay. Позже мы применим и двухстороннюю (двунаправленную) привязку, когда данные можно будет еще и редактировать.
Добавьте к разметке файла Window1.xaml контейнер TabControl с вкладкой Page1, чтобы файл стал таким<Window x:Class="DataBindingTable.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300" Width="300"
Title="Привязка элементов к БД"
MinHeight="300"
MinWidth="300"
WindowStartupLocation="CenterScreen"
Loaded="Window_Loaded"
>
<Window.Resources>
<SolidColorBrush x:Key="ControlColorBrush"
Color="{x:Static SystemColors.ControlColor}" />
</Window.Resources>
<Grid Background="{StaticResource ResourceKey=ControlColorBrush}">
<TabControl>
<!-- Привязка к таблице Employees через объект -->
<TabItem Header="Page1">
<Grid
Name="gridEmployees1"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees1"
SelectionChanged="listEmployeeslab72_SelectionChanged"
/>
<TextBlock Grid.Row="1" Margin="0,0,5,0">EmployeeID:</TextBlock>
<TextBox Grid.Row="1" Grid.Column="1"
Text="{Binding Path=EmployeeID, Mode=OneWay}"
/>
<TextBlock Grid.Row="2">FullName:</TextBlock>
<TextBox Grid.Row="2" Grid.Column="1"
Text="{Binding Path=FullName, Mode=OneWay}"
/>
<TextBlock Grid.Row="3"></TextBlock>
<TextBox Grid.Row="3" Grid.Column="1"
Text="{Binding Path=Address, Mode=OneWay}"
/>
<TextBlock Grid.Row="4">BirthDate:</TextBlock>
<TextBox Grid.Row="4" Grid.Column="1"
Text="{Binding Path=BirthDate, Mode=OneWay}"
/>
<TextBlock Grid.Row="5">Region:</TextBlock>
<TextBox Grid.Row="5" Grid.Column="1"
Text="{Binding Path=Region, Mode=OneWay}"
/>
</Grid>
</TabItem>
</TabControl>
</Grid>
</Window>
Для отображения данных пользователю мы применили интерфейсные элементы TextBox так будет 'красивше'. Мы явно установили для них тип привязки OneWay, чтобы не было разногласий со свойствами доступа промежуточного объекта Employees1. Свойства оболочки Employees1 у нас работают только для чтения, а элементы TextBox по умолчанию имеют двунаправленный тип привязки TwoWay. Но для отображения данных в привязке можно было бы использовать и элементы TextBlock, тогда тип привязки OneWay указывать было бы необязательно, поскольку у TextBlock иной привязки и не существует - они могут только отображать содержимое.
В файле Window1.xaml вызовите контекстное меню для записей событий Loaded окна и SelectionChanged списка, чтобы создать обработчики командой Navigate to Event Handler
Заполните созданные в файле Window1.xaml.cs обработчики такusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace DataBindingTable
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
Page1();
}
#region Вкладка Page1
Employees1 employees1;
private void Page1()
{
// Загружаем данные и настраиваем источник привязки
employees1 = new Employees1();
listEmployees1.SelectedIndex = 0;
listEmployees1.Focus();
gridEmployees1.DataContext = employees1.GetEmployee(0);
listEmployees1.ItemsSource = employees1.ListFullName;
listEmployees1.DisplayMemberPath = employees1.FullName;
}
private void listEmployeeslab72_SelectionChanged(
object sender, SelectionChangedEventArgs e)
{
gridEmployees1.DataContext =
employees1.GetEmployee(listEmployees1.SelectedIndex);
}
#endregion
}
}
Приведенный код мы упаковали в секцию #region для удобства, чтобы как-то структурировать код отдельных вкладок на тот случай, если их количество станет большим в процессе выполнения упражнения.
Запустите приложение - должен получиться такой результат
При выборе элемента списка текстовые поля заполняются новыми значениями привязанных свойств объекта Employees1. Здесь есть один недостаток: при прямом редактировании текстовых полей значения в них перестают меняться - срабатывает внутренний механизм элемента TextBox. На данном этапе этот недостаток можно устранить, если просто запретить пользователю их редактировать, что согласуется с принятой нами концепцией только отображения данных.
Добавьте в дескрипторы элементов TextBox атрибут Focusable="False" следующим образом<TextBlock Grid.Row="1" Margin="0,0,5,0"<EmployeeID:</TextBlock<
<TextBox Grid.Row="1" Grid.Column="1"
Text="{Binding Path=EmployeeID, Mode=OneWay}"
Focusable="False"
/<
<TextBlock Grid.Row="2"<FullName:</TextBlock<
<TextBox Grid.Row="2" Grid.Column="1"
Text="{Binding Path=FullName, Mode=OneWay}"
Focusable="False"
/<
<TextBlock Grid.Row="3"<Address:</TextBlock<
<TextBox Grid.Row="3" Grid.Column="1"
Text="{Binding Path=Address, Mode=OneWay}"
Focusable="False"
/<
<TextBlock Grid.Row="4"<BirthDate:</TextBlock<
<TextBox Grid.Row="4" Grid.Column="1"
Text="{Binding Path=BirthDate, Mode=OneWay}"
Focusable="False"
/<
<TextBlock Grid.Row="5"<Region:</TextBlock<
<TextBox Grid.Row="5" Grid.Column="1"
Text="{Binding Path=Region, Mode=OneWay}"
Focusable="False"
/<
Запустите приложение, испытайте работу вкладки Page1, разберитесь с кодомВыполним еще одно незначительное действие: поместим уже созданные файлы с процедурным кодом в отдельный каталог.
В панели Solution Explorer вызовите контекстное меню для корневого узла DataBindingTable проекта и командой Add/New Folder добавьте папку с именем Code
В панели Solution Explorer, удерживая клавишу Ctrl, выделите в корне проекта файлы StoreNorthwindDB.cs и Employees1.cs, затем перетащите их мышью в папку Code
Запустите приложение - все продолжает нормально работать, компилятор находит перемещенные файлыВ данном примере задачу вкладки Page1 мы повторим несколько другим способом. Предварительную загрузку данных из хранилища мы поместим не в объект DataTable, а сразу в библиотечную коллекцию List<T> объектов промежуточного класса. И уже к этой коллекции будем привязывать пользовательский интерфейс. Для разнообразия, при заполнении объектов данных коллекции используем ADO.NET -класс OleDbDataReader, поскольку отображаемые данные пока будем только просматривать.
Коллекция объектов-данных List<T> будет служить тонкой оболочкой над извлеченными данными БД и существовать самостоятельно. В нее поместим данные, которые сразу явно преобразуем к нужному типу. Для отображения коллекций данных к ним можно привязывать списковые интерфейсные элементы WPF типа ListBox, ComboBox, ListView, а также элементы для отображения иерархических данных типа Menu, TreeView и др. Такие интерфейсные элементы порождены классом ItemsControl и их еще называют элементами управления элементами

Соответственно, в качестве коллекций, привязываемых к перечисленным элементам управления элементами WPF, можно использовать неотображаемые объекты, наследующие интерфейс System.Collections. ICollection (например, List<T>, ObservableCollection<T>, ArrayList, Array - массив), которые к инфраструктуре WPF могут и не относиться.
В отличие от элементов управления элементами существуют элементы, способные отображать только один объект, например TextBlock или TextBox. Их еще называют элементами управления содержимым (нечем управлять кроме единственного объекта внутри себя).
Как и ранее, вначале создадим 'объект-тонкую оболочку' над извлеченными данными хранилища, общедоступные свойства которого будем привязывать к визуальным элементам пользовательского интерфейса. Поля и приватные свойства класса-оболочки в привязке участвовать не могут. Подход с применение промежуточного класса-оболочки хорош тем, что увеличивает контроль программиста над 'сырыми' данными хранилища. В этом классе мы можем заранее привести данные к нужному типу, скомпоновать и переобъявить их требуемым образом, отсортировать, отфильтровать, сделать какие-то проверки и т.д. Одним словом, - провести предварительную инспекцию и обработку с полным контролем.
Следует заметить, что WPF, как и Windows Forms, тоже способна непосредственно привязывать интерфейсные элементы к извлеченным наборам данных DataTable и DataSet инфраструктуры ADO.NET, без всяких там классов-посредников. В этом мы убедимся чуть позднее, а пока так...
В панели Solution Explorer выделите узел Code и командой Add/Class добавьте в него файл Employee.cs с одноименным классом для хранения приведенных данных, который заполните такusing System;
namespace DataBindingTable
{
// Класс свойств доступа к данным таблицы Employees
public class Employee
{
//********************************************************
// Cвойства доступа для привязки и базовые поля
//********************************************************
int employeeID;
public int EmployeeID
{
get { return employeeID; }
}
string fullName;
public string FullName
{
get { return fullName; }
}
string address;
public string Address
{
get { return address; }
}
string birthDate;
public string BirthDate
{
get { return birthDate; }
}
string region;
public string Region
{
get { return region; }
}
// Конструктор с параметрами
public Employee(int employeeID,
string fullName, string address,
string birthDate, string region)
{
this.employeeID = employeeID;
this.fullName = fullName;
this.address = address;
this.birthDate = birthDate;
this.region = region;
}
// Конструктор по умолчанию
//public Employee() { }
}
}
Класс Employee будет служить тонкой оболочкой для загруженных из хранилища данных. Каждый экземпляр этого класса будет хранить типизированные данные одной считанной из хранилища записи. Конструктор по умолчанию создавать не стали для разнообразия, чтобы подчеркнуть, что использовать будем только параметризованный конструктор.
В файле StoreNorthwindDB.cs добавьте в класс StoreNorthwindDB метод с именем ReadTableEmployees() для заполнения библиотечной коллекции объектов типизированных данных//*********************************************************
// Метод извлечения данных из таблицы Employees
// хранилища базы данных в объект-коллекцию List<T>,
// который называется типизированным списком
//*********************************************************
private List<Employee> employees = null;// Типизированный список
public List<Employee> ReadTableEmployees()
{
// Читаем данные из хранилища только один раз
if (employees != null)
return employees;
// Настраиваем доступ к хранилищу
String cmdText = "SELECT EmployeeID, " +
"(LastName + ', ' + FirstName) AS FullName, " +
"Address, BirthDate, Region FROM Employees";
OleDbConnection conn = new OleDbConnection(connectionString);
OleDbCommand cmd = new OleDbCommand(cmdText, conn);
cmd.CommandType = CommandType.Text;
// Заполняем коллекцию данными из хранилища
employees = new List<Employee>();
try
{
conn.Open();
OleDbDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
// Создать объект Employee, являющийся
// оболочкой для текущей записи таблицы Employees
Employee employee = new Employee(
Convert.ToInt32(reader["EmployeeID"]),
Convert.ToString(reader["FullName"]),
Convert.ToString(reader["Address"]),
Convert.ToString(reader["BirthDate"]),
Convert.ToString(reader["Region"]));
// Добавить в коллекцию
employees.Add(employee);
}
}
catch
{
MessageBox.Show("Ошибка загрузки данных");
}
finally
{
conn.Close(); // Закрываем соединение
}
return employees;
}
Метод ExecuteReader() устанавливает связь с нужной таблицей предварительно открытого соединения с БД в соответствии с настройками SQL -запроса и курсор данных таблицы вначале установлен на первую запись. Метод Read() считывает текущую запись таблицы и перемещает курсор на следующую запись. Цикл чтения записей таблицы продолжается до тех пор, пока курсор данных не установится за последней записью. Тогда метод Read() возвратит false и цикл чтения данных прервется. Пока данные считываются построчно, соединение с БД должно быть открытым. В отличие от метода OleDbDataAdapter.Fill(), соединение с БД при применении класса OleDbDataReader приходится открывать и закрывать вручную. За этим нужно строго следить, иначе данные для других пользователей останутся блокированными.
Добавьте в контейнер TabControl новую вкладку с именем Page2 с привязками интерфейсных элементов к общедоступным свойствам объекта данных Employee<!-- Привязка к таблице Employees через коллекцию объектов -->
<TabItem Header="Page2">
<Grid
Name="gridEmployees2"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees2"
SelectionChanged="listEmployees2_SelectionChanged"
/>
<TextBlock Grid.Row="1" Margin="0,0,5,0">EmployeeID:</TextBlock>
<TextBox Grid.Row="1" Grid.Column="1"
Text="{Binding Path=EmployeeID, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="2">FullName:</TextBlock>
<TextBox Grid.Row="2" Grid.Column="1"
Text="{Binding Path=FullName, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="3">Address:</TextBlock>
<TextBox Grid.Row="3" Grid.Column="1"
Text="{Binding Path=Address, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="4">BirthDate:</TextBlock>
<TextBox Grid.Row="4" Grid.Column="1"
Text="{Binding Path=BirthDate, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="5">Region:</TextBlock>
<TextBox Grid.Row="5" Grid.Column="1"
Text="{Binding Path=Region, Mode=OneWay}"
Focusable="False"
/>
</Grid>
</TabItem>
В выражениях привязки свойства Text элементов TextBox опущено указание источника данных, поэтому WPF будет искать его в свойстве DataContext ближайших родительских элементов. В процедурном коде мы определим источник в свойстве DataContext для элемента gridEmployees2.
Добавьте в класс Window1 файла Window1.xaml.cs следующий блок кода, управляющий привязкой интерфейсных элементов вкладки Page2#region Вкладка Page2
List<Employee> employees2;// Ссылка на типизированный список
private void Page2()
{
// Загружаем данные и настраиваем источник привязки
employees2 = App.StoreNorthwindDB.ReadTableEmployees();
listEmployees2.SelectedIndex = 0;
listEmployees2.Focus();
gridEmployees2.DataContext = employees2[0];
listEmployees2.ItemsSource = employees2;
listEmployees2.DisplayMemberPath = "FullName";
}
private void listEmployees2_SelectionChanged(
object sender, SelectionChangedEventArgs e)
{
gridEmployees2.DataContext =
employees2[listEmployees2.SelectedIndex];
}
#endregion
Здесь мы опять для удобства сгруппировали код в секцию #region. Обработчик события SelectionChanged меняет значение свойства DataContext сетки при выделении элементов списка. Тем самым будет меняться источник, к которому в разметке привязаны элементы TextBox, и соответственно будут меняться значения свойств Text, показывая новые данные.
Поместите в обработчик Window_Loaded() класса Window1 вызов функции Page2()private void Window_Loaded(object sender, RoutedEventArgs e)
{
Page1();
Page2();
}
Запустите приложение и испытайте работу вкладки Page2 - получим точно такую же функциональность, что и для Page1
Разберитесь с кодом рассмотренного примераС последним примером можно немного поэкспериментировать, перенеся привязку списка ListBox из кода в разметку.
Добавьте к разметке элемента ListBox вкладки Page2 в файле Window1.xaml следующую строку привязки<ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees2"
SelectionChanged="listEmployees2_SelectionChanged"
ItemsSource="{Binding}" DisplayMemberPath="FullName"
/>
Модифицируйте в классе Window1 файла Window1.xaml.cs функцию Page2() следующим образомprivate void Page2()
{
// Загружаем данные и назначаем источник привязки
employees2 = App.StoreNorthwindDB.ReadTableEmployees();
listEmployees2.SelectedIndex = 0;
listEmployees2.Focus();
gridEmployees2.DataContext = employees2[0];
//listEmployees2.ItemsSource = employees2;
//listEmployees2.DisplayMemberPath = "FullName";
listEmployees2.DataContext = employees2;
}
В представлении XAML элемента списка свойство ItemsSource мы задали для объекта Binding без указания атрибутов. Поскольку свойство Source объекта Binding не задано, WPF начнет искать источник в свойстве DataContext текущего элемента, а при неудаче - далее будет искать его в родительских элементах вверх по визуальному дереву в направлении его корня. В процедурном коде мы присваиваем свойству DataContext значение объекта-коллекции employees2, тем самым указываем источник связывания в самом элементе списка listEmployees2. Свойство DisplayMemberPath="FullName" списка определяет столбец коллекции-источника, который будет отображаться на экране. В нашем случае - это вычислимый столбец таблицы ADO.NET.
Запустите приложение и испытайте работу вкладки Page2 - ее функциональность осталась прежнейМожно пойти еще дальше в привязке на стороне разметки. Учитывая, что привязанные к элементу ListBox данные синхронно позиционирует свой внутренний курсор построчно в соответсвии с выделенным элементом SelectedItem списка, к этому элементу мы и привяжем свойство Grid.DataContext как к общему источнику данных для текстовых полей TextBox. Получится, что список (элемент управления элементами) привязан к коллекции, а элементы управления содержимым привязаны не к элементам коллекции, а к элементам списка.
Выполните в открывающем дескрипторе элемента Grid вкладки Page2 следующие изменения| Было | <Grid
Name="gridEmployees2"
>
|
|---|---|
| Стало | <Grid
DataContext="{Binding ElementName=listEmployees2, Path=SelectedItem}"
>
|
Удалите, за ненадобностью, из дескриптора элемента ListBox подписку обработчика на событие SelectionChanged| Было | <ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees2"
SelectionChanged="listEmployees2_SelectionChanged"
ItemsSource="{Binding}" DisplayMemberPath="FullName"
/>
|
|---|---|
| Стало | <ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees2"
ItemsSource="{Binding}" DisplayMemberPath="FullName"
/>
|
Удалите полностью, за ненадобностью, из класса Window1 файла Window1.xaml обработчик listEmployees2_SelectionChanged() или закомментируйте содержимое его тела
Закомментируйте в функции Page2() файла Window1.xaml.cs строку кода присвоения свойству DataContext начального элемента коллекции данныхprivate void Page2()
{
// Загружаем данные и назначаем источник привязки
employees2 = App.StoreNorthwindDB.ReadTableEmployees();
listEmployees2.SelectedIndex = 0;
listEmployees2.Focus();
//gridEmployees2.DataContext = employees2[0];
//listEmployees2.ItemsSource = employees2;
//listEmployees2.DisplayMemberPath = "FullName";
listEmployees2.DataContext = employees2;
}
Запустите приложение и испытайте работу вкладки Page2 - она продолжает работать как ни в чем небывалоЧто здесь получилось. Коллекцию Employees объектов Employee, которая инкапсулирует извлеченные из БД данные, мы оставили подключенной к DataContext списка ListBox и ее найдет свойство ItemsSource списка. А вот источником привязки для текстовых полей мы назначили свойство SelectedItem списка, которое ассоциируется с элементом коллекции, и разместили эту информацию в свойстве DataContext сетки Grid. Ее найдут привязанные текстовые поля. Фактически мы сделали то же самое, что выполнял ранее процедурный код в обработчике события SelectionChanged списка.
Вдоль дороги лес густой - с Бабами-Ягами!
А в конце дороги той - плаха с топорами!
(В.С. Высоцкий)
Здесь побочной работы будет мало, поскольку у нас уже есть метод LoadTableEmployees() в классе StoreNorthwindDB, возвращающий заполненный объект DataTable. Только ранее мы его применяли в объекте-оболочке Employees1, а потом этот объект Employees1 использовали в классе Window1 файла Window1.xaml.cs. Теперь метод LoadTableEmployees() вызовем в классе Window1 и возвращенный им объект-коллекцию DataTable используем в привязке того же самого набора интерфейсных элементов WPF.
Есть одна тонкость в намерении применить уже готовый метод LoadTableEmployees(). Мы сгоряча в нем сделали проверку, чтобы объект DataTable создавался только один раз, из благих намерений - желая разгрузить наш компьютер. Но это значит, что к каким бы ссылкам мы этот метод не применяли, все они будут адресовать один и тот же физически существующий объект DataTable. Пока что это не страшно, поскольку извлеченные из таблицы БД данные мы только просматриваем, а не редактируем. И неважно, через какие замочные скважины (вкладки Pages) мы за этими данными будем подглядывать.
Создайте в файле Window1.xaml из копии вкладки Page2 (в ее последней версии после проведенных модификаций) новую вкладку Page3 и сделайте в ней необходимые переименования элементов<!-- Привязка к таблице Employees через объект DataTable из ADO.NET -->
<TabItem Header="Page3">
<Grid
DataContext="{Binding ElementName=listEmployees3, Path=SelectedItem}"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees3"
ItemsSource="{Binding}" DisplayMemberPath="FullName"
/>
<TextBlock Grid.Row="1" Margin="0,0,5,0">EmployeeID:</TextBlock>
<TextBox Grid.Row="1" Grid.Column="1"
Text="{Binding Path=EmployeeID, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="2">FullName:</TextBlock>
<TextBox Grid.Row="2" Grid.Column="1"
Text="{Binding Path=FullName, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="3">Address:</TextBlock>
<TextBox Grid.Row="3" Grid.Column="1"
Text="{Binding Path=Address, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="4">BirthDate:</TextBlock>
<TextBox Grid.Row="4" Grid.Column="1"
Text="{Binding Path=BirthDate, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="5">Region:</TextBlock>
<TextBox Grid.Row="5" Grid.Column="1"
Text="{Binding Path=Region, Mode=OneWay}"
Focusable="False"
/>
</Grid>
</TabItem>
Добавьте в класс Window1 файла Window1.xaml.cs код присоединения коллекции DataTable к интерфейсным элементам вкладки Page3#region Вкладка Page3
private void Page3()
{
// Настраиваем списковый элемент ListBox
listEmployees3.SelectedIndex = 0;
listEmployees3.Focus();
// Назначаем источником объект-коллекцию данных DataTable
listEmployees3.DataContext =
App.StoreNorthwindDB.LoadTableEmployees();
}
#endregion
Опять для удобства мы применили секцию #region.
Поместите в обработчик Window_Loaded() класса Window1 вызов функции Page3() для инициализации списка listEmployees3 вкладки Page3private void Window_Loaded(object sender, RoutedEventArgs e)
{
Page1();
Page2();
Page3();
}
Запустите приложение и испытайте работу вкладки Page3 - получим точно такую же функциональность, что и для двух предыдущих вкладок
Разницы нет между чистою правдой и ложью,
Если вначале и ту и другую раздеть!
(В.С. Высоцкий)
Источником для свойства DataContext можно назначить набор DataSet инфраструктуры ADO.NET, а не DataTable. DataSet считается аналогом реляционной БД, только находится в оперативной памяти. Поскольку объект DataSet способен одновременно инкапсулировать много различных таблиц-объектов DataTable (и воспроизводить отношения между ними), то в выражениях привязки интерфейсных элементов XAML следует делать уточнения, к какой именно таблице набора привязываться. Рассмотрим это на примере вкладки Page4.
Загрузим в набор данных ADO.NET три таблицы сразу, которые в дальнейшем, при необходимости, можно будет использовать для рассмотрения других способов привязки. Но в текущей вкладке Page4 привяжем для отображения только одну таблицу Employees, чтобы глубже почувствовать разницу с предыдущими способами на одинаковом результате. Мы уже неоднократно убеждались в том, что .NET Framework позволяет решать одну и ту же задачу разными способами - поистине мощная библиотека.
Дополните класс StoreNorthwindDB в одноименном файле новым методом ReadDataSet() со следующим содержимым//*********************************************************
// Метод извлечения нетипизированного набора
// данных из хранилища данных Northwind.mdb
//*********************************************************
DataSet ds = null;// Ссылка на нетипизированный набор DataSet
public DataSet ReadDataSet()
{
// Загрузим набор данных только один раз
if (ds != null)
return ds;
ds = new DataSet();// Создаем множественный набор данных
// Заполняем множественный набор данных из БД
using (OleDbConnection conn = new OleDbConnection(connectionString))
{
OleDbCommand selectCommand = conn.CreateCommand();
OleDbDataAdapter adapter = new OleDbDataAdapter(selectCommand);
// Загружаем всю таблицу Employees
selectCommand.CommandText = "SELECT * FROM Employees";
adapter.Fill(ds);// Сам открывает, загружает и закрывает
// Назначаем загруженным данным в наборе такое же имя,
// как и в хранилище, чтобы в дальнейшем не путаться
ds.Tables[0].TableName = "Employees";
// Загружает всю таблицу Customers
selectCommand.CommandText = "SELECT * FROM Customers";
// Назначаем загружаемым данным в наборе имя, в этом способе
// надо переопределить дежурное имя Table до заполнения набора
adapter.TableMappings.Add("Table", "Customers");// Должен стоять перед
adapter.Fill(ds); // Должен стоять после
// Загружает всю таблицу Orders
selectCommand.CommandText = "SELECT * FROM Orders";
// Загружаем и сразу этой порции данных назначаем имя в наборе
adapter.Fill(ds, "Orders");
// Для проверки способов именования в панели Output режима Debug
System.Diagnostics.Debug.WriteLine(ds.Tables[0].TableName);
System.Diagnostics.Debug.WriteLine(ds.Tables[1].TableName);
System.Diagnostics.Debug.WriteLine(ds.Tables[2].TableName);
return ds;
}
}
Чтобы развеять сомнения в том, что таблицы действительно загружены в набор данных и получили заданные имена, в конце метода ReadDataSet() добавлен отладочный код, который работает только для режима Debug. Он выводит результат в панель Output оболочки и его удалять необязательно, потому что в рабочем режиме Release он сам отключится.
Чтобы проверить работу метода, вставьте его вызов в обработчик Window_Loaded() класса Window1 файла Window1.xaml.cs такprivate void Window_Loaded(object sender, RoutedEventArgs e)
{
Page1();
Page2();
Page3();
App.StoreNorthwindDB.ReadDataSet();
}
Запустите проект и проверьте отладочный вывод в панели Output (панель можно открыть командой меню оболочки View/Output )В конце отладочной информации найдем строки
Employees
Customers
Orders
Можно для пробы поменять имена таблиц TableName в наборе данных объектной модели метода и убедиться, что код верно реагирует на эти изменения. При этом строки самих SQL -запросов трогать не следует, поскольку это зона ответственности самого хранилища данных, а не объектной модели ADO.NET.
Теперь осталось сделать вкладку Page4 для размещения интерфейсных элементов отображения данных и привязать к ним заполненный набор данных.
Добавьте в контейнер TabControl файла Window1.xaml копию вкладки Page3 и сделайте очевидные переименования, чтобы разметка новой вкладки Page4 стала такой<!-- Привязка к таблице Employees через объект DataSet из ADO.NET -->
<TabItem Header="Page4">
<Grid
DataContext="{Binding ElementName=listEmployees4, Path=SelectedItem}"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees4"
ItemsSource="{Binding}" DisplayMemberPath="FullName"
/>
<TextBlock Grid.Row="1" Margin="0,0,5,0">EmployeeID:</TextBlock>
<TextBox Grid.Row="1" Grid.Column="1"
Text="{Binding Path=EmployeeID, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="2">FullName:</TextBlock>
<TextBox Grid.Row="2" Grid.Column="1"
Text="{Binding Path=FullName, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="3">Address:</TextBlock>
<TextBox Grid.Row="3" Grid.Column="1"
Text="{Binding Path=Address, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="4">BirthDate:</TextBlock>
<TextBox Grid.Row="4" Grid.Column="1"
Text="{Binding Path=BirthDate, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="5">Region:</TextBlock>
<TextBox Grid.Row="5" Grid.Column="1"
Text="{Binding Path=Region, Mode=OneWay}"
Focusable="False"
/>
</Grid>
</TabItem>
Заметим, что список ListBox имеет имя для того, чтобы в процедурном коде к нему можно было обратиться и присвоить свойству DataContext значение источника привязки - набора данных.
#region Вкладка Page4
private void Page4()
{
// Настраиваем списковый элемент ListBox
listEmployees4.SelectedIndex = 0;
listEmployees4.Focus();
// Назначаем источником набор данных DataSet
listEmployees4.DataContext =
App.StoreNorthwindDB.ReadDataSet();
}
#endregion
Примените в обработчике события Loaded класса Window1 вызов функции Page4()private void Window_Loaded(object sender, RoutedEventArgs e)
{
Page1();
Page2();
Page3();
//App.StoreNorthwindDB.ReadDataSet();
Page4();
}
Измените дескриптор списка вкладки Page4 следующим образом| Было | <ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees4"
ItemsSource="{Binding}" DisplayMemberPath="FullName"
/>
|
|---|---|
| Стало | <ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees4"
ItemsSource="{Binding Path=Employees}" DisplayMemberPath="FullName"
/>
|
Запустите приложение - вкладка Page4 стала работать, но только частично
Текстовые поля TextBox функционируют (кроме поля FullName ) и список тоже, только в нем не отображается информация, установленная в настройках DisplayMemberPath="FullName".
При запущенном приложении загляните в панель Output оболочкиМы видим, что механизм привязки хоть и не возбуждает исключений, но под управлением оболочки все-таки сигнализирует об ошибке, выдавая серию предупреждений, примерно таких
System.Windows.Data Error: 39 : BindingExpression path error:
'FullName' property not found on 'object' ''DataRowView' (HashCode=48835636)'.
BindingExpression:Path=FullName; DataItem='DataRowView' (HashCode=48835636);
target element is 'TextBox' (Name=''); target property is 'Text' (type 'String')
Установите в разметке настроек списка ListBox вкладки Page4 другое значение отображаемого столбца объектной модели источника| Было | <ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees4"
ItemsSource="{Binding Path=Employees}" DisplayMemberPath="FullName"
/>
|
|---|---|
| Стало | <ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees4"
ItemsSource="{Binding Path=Employees}" DisplayMemberPath="FirstName"
/>
|
Запустите приложение - список стал отображать столбец FirstName, который был загружен из хранилища, а текстовый элемент для поля FullName, которое мы хотели показать, попрежнему остается пустым
Причина тут в том, что физически столбца с именем FullName ни в хранилище, ни в объектной модели набора данных просто не сущестует. Он является вычислимым, то есть формируется налету при извлечении данных. Ранее мы его могли использовать потому, что заранее придумали в выражении SQL -запроса и он попадал в коллекцию источника уже заполненным.
Сейчас вычислимый столбец тоже можно добавить в нужную коллекцию объектной модели набора, но поскольку данные уже заполнены, придется заново перебрать все строки коллекции и присвоить придуманному столбцу требуемыми значениями. А можно поступить по другому: добавить в коллекцию набора ДО ЕГО ЗАПОЛНЕНИЯ новый столбец, настроить его схему требуемым образом, а потом загрузить коллекцию данными из хранилища. Вычислимый столбец сразу будет пересчитан налету и заполнен данными. При этом выражение SQL-запроса менять не придется. Проверим оба способа.
Верните свойство отображаемого столбца списка в прежнее значение DisplayMemberPath="FullName", где опять будет фигурировать имя несуществующего столбца коллекции Employees
Дополните метод ReadDataSet() следующими двумя закомментированными блоками кода, каждый из которых готов создать вычислимый столбец FullName//*********************************************************
// Метод извлечения нетипизированного набора
// данных из хранилища данных Northwind.mdb
//*********************************************************
DataSet ds = null;// Ссылка на нетипизированный набор DataSet
public DataSet ReadDataSet()
{
// Загрузим набор данных только один раз
if (ds != null)
return ds;
ds = new DataSet();// Создаем множественный набор данных
// Заполняем множественный набор данных из БД
using (OleDbConnection conn = new OleDbConnection(connectionString))
{
OleDbCommand selectCommand = conn.CreateCommand();
OleDbDataAdapter adapter = new OleDbDataAdapter(selectCommand);
/*//////////////////////////////////////
// Добавления вычислимого столбца в еще
// незаполненную коллекцию объектной модели
////////////////////////////////////////
// Готовим вычислимый столбец FullName
// для объекта-коллекции Employees
// ДО ЕЕ ЗАПОЛНЕНИЯ В НАБОРЕ ДАННЫХ !!!!!
ds.Tables.Add(new DataTable("Table"));// Дежурное имя по умолчанию
DataColumn column = new DataColumn("FirstName");
column.DataType = typeof(string);
ds.Tables[0].Columns.Add(column);
//
column = new DataColumn();
column.ColumnName = "LastName";
column.DataType = System.Type.GetType("System.String");
ds.Tables[0].Columns.Add(column);
//
column = new DataColumn("FullName", typeof(String));
column.Expression = "LastName + ', ' + FirstName";
ds.Tables[0].Columns.Add(column);
//*/////////////////////////////////////
// Загружаем всю таблицу Employees
selectCommand.CommandText = "SELECT * FROM Employees";
adapter.Fill(ds);// Сам открывает, загружает и закрывает
// Назначаем загруженным данным в наборе такое же имя,
// как и в хранилище, чтобы в дальнейшем не путаться
ds.Tables[0].TableName = "Employees";
// Загружает всю таблицу Customers
selectCommand.CommandText = "SELECT * FROM Customers";
// Назначаем загружаемым данным в наборе имя, в этом способе
// надо переопределить дежурное имя Table до заполнения набора
adapter.TableMappings.Add("Table", "Customers");// Должен стоять перед
adapter.Fill(ds); // Должен стоять после
// Загружает всю таблицу Orders
selectCommand.CommandText = "SELECT * FROM Orders";
// Загружаем и сразу этой порции данных назначаем имя в наборе
adapter.Fill(ds, "Orders");
/*//////////////////////////////////////
// Добавления вычислимого столбца в уже
// заполненную коллекцию объектной модели
////////////////////////////////////////
// Дублируем ссылку для удобства адресации
DataTable tableEmployees = ds.Tables["Employees"];
DataColumn column = new DataColumn("FullName");
column.DataType = typeof(string);
tableEmployees.Columns.Add(column);
// Приведем к верхнему регистру для различения
foreach (DataRow row in tableEmployees.Rows)
row[column] = row["LastName"].ToString().ToUpper() + ", " +
row["FirstName"].ToString().ToUpper();
//*/////////////////////////////////////
return ds;
}
}
Раскомментируйте поочередно один из блоков (но не оба сразу!) создания вычислимого столбца и испытайте работу приложения. Быстрый способ раскомментировать любой из приведенных блоков - добавить слэш перед открывающей скобкой многострочного комментария (перед звездочкой)При реализации любого из рассмотренных способов вкладка Page4 будет выглядеть так, как и ожидалось
В предыдущем примере мы привязывали набор данных к именованному списку, а затем выделенный элемент SelectedItem списка назначали в свойстве DataContext сетки Grid. К этому SelectedItem привязывались текстовые поля TextBox. Объект SelectedItem менялся при выборе пользователя, а следом менялись и значения TextBox. В этом примере набор данных мы присвоим свойству DataContext именованного элемента Grid, чтобы его там нашел и список, и текстовые поля.
Добавьте к интерфейсной коллекции TabControl копию вкладки Page4 и преобразуйте ее во вкладку Page5 так<!-- Прямая привязка элементов к набору данных DataSet -->
<TabItem Header="Page5">
<Grid Name="grid5"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ItemsSource="{Binding Path=Employees}"
DisplayMemberPath="FullName"
IsSynchronizedWithCurrentItem="True"
/>
<TextBlock Grid.Row="1" Margin="0,0,5,0">EmployeeID:</TextBlock>
<TextBox Grid.Row="1" Grid.Column="1"
Text="{Binding Path=Employees/EmployeeID, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="2">FullName:</TextBlock>
<TextBox Grid.Row="2" Grid.Column="1"
Text="{Binding Path=Employees/FullName, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="3">Address:</TextBlock>
<TextBox Grid.Row="3" Grid.Column="1"
Text="{Binding Path=Employees/Address, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="4">BirthDate:</TextBlock>
<TextBox Grid.Row="4" Grid.Column="1"
Text="{Binding Path=Employees/BirthDate, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="5">Region:</TextBlock>
<TextBox Grid.Row="5" Grid.Column="1"
Text="{Binding Path=Employees/Region, Mode=OneWay}"
Focusable="False"
/>
</Grid>
</TabItem>
Мы назначили элементу Grid имя для возможности присвоения в процедурном коде его свойству DataContext ссылки на набор данных. Это будет единственный именованный элемент в разметке вкладки Page5, потому что другие элементы мы программировать в процедурном коде не будем. В списке ListBox мы оставили неизменными свойство Path=Employees, означающее таблицу набора в элементе Grid.DataContext, к которой привяжется список, и - отображаемый вычислимый столбец FullName.
Имя списка мы убрали, но добавили атрибут IsSynchronizedWithCurrentItem="True", который заставляет синхронизировать перемещение курсора в коллекции Employees набора данных со сменой выделенного пользователем элемента списка. Привязываемое к набору свойство Text элементов TextBox должно конкретизировать коллекцию в наборе Grid.DataContext вместе с адресуемым свойством привязки.
Добавьте в класс Window1 файла Window1.xaml процедурный код для инициализации источника привязки в именованной сетке Grid следующим образомprivate void Window_Loaded(object sender, RoutedEventArgs e)
{
Page1();
Page2();
Page3();
Page4();
Page5();
}
....................................................
#region Вкладка Page5
private void Page5()
{
// Назначаем источником набор данных DataSet в сетке
grid5.DataContext =
App.StoreNorthwindDB.ReadDataSet();
}
#endregion
Запустите приложение - вкладка Page5 работает
Попробуйте убрать из списка атрибут IsSynchronizedWithCurrentItem="True" или заменить его значение на False - текстовые поля перестают отслеживать выделенные позиции списка: список и коллекция прекратили синхронную работу
Сейчас редкое приложение обходится без данных, хранящихся в центральном большом хранилище (Store - хранилище), - базе данных (БД). Такие приложения обычно называют управляемые данными. Их можно построить и по технологии WPF. Для взаимодействия с БД традиционно используются возможности объектов ADO.NET, а вот для отображения данных можно применить элементы и механизмы WPF, в том числе - привязку.
Главная трудность, которая нас ожидает при выполнении упражнений данной темы, - вспомнить классы ADO.NET и правильно их использовать. Такая ситуация естественна, пока не накопилось нужного количества практического опыта, а это дело наживное. Тем более, что необходимый для понимания данной темы минимум сведений по ADO.NET был изложен в начале этого курса, и к нему всегда, при необходимости, можно вернуться.
Начнем с привязки списковых элементов WPF к табличным объектам ADO.NET для простого просмотра. Нам нужно будет извлечь данные из БД и заполнить ими некоторый объект, способный автономно хранить добытые данные в оперативной памяти, не занимая при этом соединение с базой данных. Затем следует связать какой-нибудь списковый интерфейсный элемент WPF с этим объектом - источником данных.
В качестве хранилища данных будем использовать учебную базу данных Northwind.mdb типа OLE DB (Object Linking and
Более того, в данном упражнении пока будем работать только с таблицей Employees БД Northwind.mdb (для единообразия). Постепенно перейдем и к другим таблицам, особенно при рассмотрении вопросов работы со взаимосвязанными таблицами БД, находящимися в отношении (Relations) "главная-подробности" (" master-detail ").
Для удобства, еще раз приведем схему, отображающую структуру БД Northwind.mdb, с которой мы ранее уже встречались

Прежде, чем двигаться дальше, уместно напомнить о существовании многоуровневого подхода к проектированию приложений, когда функциональные возможности, компоненты взаимодействия с пользователем и код проекта в целом искусственно делятся на отдельные логические уровни.
Уровней может быть несколько в зависимости от размера проекта, но обычно их бывает четыре. Об этом мы раньше говорили, но здесь еще раз вспомним:
Очень часто границы между уровнями размыты, но все же надо их уметь распознавать или искусственно придумывать по какому-нибудь признаку, чтобы структурировать код приложения. Это дисциплинирует программиста, делает отдельные части кода более независимыми и уменьшает количество возможных ошибок. Старый методологический принцип - разделяй и властвуй (в данном случае - вертикаль власти), так хорошо помогающий нашему, порой еще слабосильному, сознанию.
Перечисленные уровни стремятся изолировать друг от друга, упаковывая код в отдельные файлы, динамические библиотеки, классы и пространства имен. Такой способ позволяет скрыть основную массу деталей и оставить только интерфейсные члены для взаимодействия с другими частями приложения. Подобным же образом намерены поступать и мы (клянемся!).
Командой File/New/Project создайте новое решение DataBindingTable с одноименным проектом
В панели Solution Explorer выделите корень проекта и командой Add/New Folder контекстного меню создайте подкаталог Data
Через контекстное меню панели Solution Explorer скопируйте из прилагаемой папки Source файл Northwind.mdb командой Add/Existing Item (измените фильтр диалогового окна на All Files). При появлении мастера Data Source Configuration Wizard отмените его кнопкой Cancel, поскольку мы пока не собираемся создавать типизированный набор данных
Выделите в панели Solution Explorer файл Northwind.mdb и через панель Properties проверьте, что его свойства имеют значениеBuild Action=ContentCopy to Output Directory=Copy always
В панели Solution Explorer добавьте для корневого узла проекта командой Add/New Item заготовку файла конфигурации приложения с именем App.config
Заполните конфигурационный файл App.config кодом настройки строки соединения с БД, значение которой мы будем извлекать в процедурном коде во время выполнения приложения с помощью класса ConfigurationManager<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="MyNorthwind"
connectionString="Provider=Microsoft.Jet.OLEDB.4.0;
Data Source=|DataDirectory|\Data\Northwind.mdb"
providerName="System.Data.OleDb" />
</connectionStrings>
</configuration>
В панели Solution Explorer вызовите для узла References проекта контекстное меню и командой Add Reference добавьте ссылку на библиотечную сборку System.Configuration.dll, в которой находится нужный нам класс ConfigurationManager для работы с конфигурационным файлом App.config из процедурного кода
Выделите в панели Solution Explorer узел проекта и добавьте к нему командой Project/Add Class новый файл с именем StoreNorthwindDB.cs
Добавьте в начало файла StoreNorthwindDB.cs строку подключения пространств имен инфраструктуры ADO.NET и заполните класс StoreNorthwindDB следующим кодомusing System;
using System.Collections.Generic;
using System.Text;
// Подключение пространств имен инфраструктуры ADO.NET
using System.Data;
using System.Data.OleDb;
using System.Windows;// Для MessageBox
namespace DataBindingTable
{
// Класс для доступа к БД
public class StoreNorthwindDB
{
// Извлекаем в поле строку соединения из файла App.config
String connectionString = System.Configuration.
ConfigurationManager.ConnectionStrings["MyNorthwind"].ConnectionString;
//*********************************************************
// Метод извлечения данных из таблицы Employees
// хранилища базы данных в ADO.NET-объект DataTable
//*********************************************************
DataTable dtEmployees = null;// Ссылка на объект DataTable
public DataTable LoadTableEmployees()
{
// Загрузим таблицу Employees только один раз
if (dtEmployees != null)
return dtEmployees;
// Заполняем объект таблицы Employees данными из БД
dtEmployees = new DataTable();
using (OleDbConnection conn = new OleDbConnection(connectionString))
{
OleDbCommand selectCommand = conn.CreateCommand();
OleDbDataAdapter adapter = new OleDbDataAdapter(selectCommand);
// Загружает данные и схему таблицы Employees
selectCommand.CommandText = "SELECT EmployeeID, " +
"(LastName + ', ' + FirstName) AS FullName, " +
"Address, BirthDate, Region FROM Employees";
try
{
// Метод сам открывает БД и сам же ее закрывает
adapter.Fill(dtEmployees);
}
catch
{
MessageBox.Show("Ошибка подключения к БД");
}
finally
{
conn.Close(); // На всякий случай!
}
}
return dtEmployees;
}
}
}
Инструкция using(), в которой создается соединение с БД, автоматически закроет его после выполнения своего блока кода, хотя то же самое сделает и метод adapter.Fill(). В SQL-запросе к БД наряду с извлечением столбцов оригинальных мы одновременно формируем вычислимый столбец FullName. Метод LoadTableEmployees(), который будет вызываться в клиенте класса, вернет ссылку на объект dtEmployees типа DataTable, загруженный данными таблицы.
Для того, чтобы получить только один экземпляр класса StoreNorthwindDB, и чтобы он был доступен во всех окнах приложения, разместим код его создания в классе App.
Откройте файл App.xaml.cs и добавьте в класс App уровня приложения следующий кодusing System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Windows;
namespace DataBindingTable
{
public partial class App : Application
{
// Базовое поле для свойства
private static StoreNorthwindDB storeNorthwindDB =
new StoreNorthwindDB();
// Свойство для базового поля со ссылкой на экземпляр класса
public static StoreNorthwindDB StoreNorthwindDB
{
get { return storeNorthwindDB; }
}
}
}
Теперь нам нужно создать промежуточный класс, который бы инкапсулировал поля загруженного данными объекта DataTable в общедоступные свойства. К общедоступным свойствам мы будем привязывать интерфейсные элементы пока для просмотра данных. Именно, только для общедоступных свойств привязка к пользовательскому классу будет работать. Вспомогательный класс разместим в отдельном файле.
В панели Solution Explorer для узла проекта DataBindingTable вызовите контекстное меню и командой Add/Class создайте файл Employees1.cs
Заполните файл Employees1.cs такusing System;
using System.Collections.Generic;
using System.Text;
// Подключение пространства имен для DataTable
using System.Data;
namespace DataBindingTable
{
// Класс свойств доступа к данным таблицы Employees
class Employees1
{
//********************************************************
// Cвойства доступа для привязки и базовые поля
//********************************************************
// Для списка надо массив
string[] listFullName = null;
public string[] ListFullName
{
get
{
if (listFullName == null)
{
listFullName = new string[dt.Rows.Count];
int i = 0;
// Перебираем записи и заполняем массив для списка
foreach (DataRow row in dt.Rows)
{
listFullName[i] = (string)dt.Rows[i]["FullName"];
i++;
}
}
return listFullName;
}
}
int employeeID;
public int EmployeeID
{
get { return employeeID; }
}
string fullName;
public string FullName
{
get { return fullName; }
}
string address;
public string Address
{
get { return address; }
}
string birthDate;
public string BirthDate
{
get { return birthDate; }
}
string region;
public string Region
{
get { return region; }
}
// Навигация по таблице ADO.NET
public Employees1 GetEmployee(int ID)
{
// Проверяем границы массива
if (ID < 0 || ID >= dt.Rows.Count)
return null;
// Наполняем свойства полями записи ID
// для последующего их извлечения свойствами
// в привязанные элементы интерфейса.
// Входные параметры приводятся явно
// к тому типу, который ожидает метод
return new Employees1(
(int)dt.Rows[ID]["EmployeeID"],
(string)dt.Rows[ID]["FullName"],
(string)dt.Rows[ID]["Address"],
dt.Rows[ID]["BirthDate"].ToString(),
dt.Rows[ID]["Region"].ToString()
);
}
// Конструктор с параметрами
public Employees1(int employeeID,
string fullName, string address,
string birthDate, string region)
{
this.employeeID = employeeID;
this.fullName = fullName;
this.address = address;
this.birthDate = birthDate;
this.region = region;
}
// Конструктор по умолчанию
public Employees1() { }
// Ссылка на исходную таблицу-объект ADO.NET
DataTable dt = App.StoreNorthwindDB.LoadTableEmployees();
}
}
Обратите внимание, что в классе Employees1 мы предусмотрели свойства доступа только с аксессором get, значит инкапсулированные данные пока можно будет только просматривать и тип привязки интерфейсных элементов-приемников должен быть только OneWay. Позже мы применим и двухстороннюю (двунаправленную) привязку, когда данные можно будет еще и редактировать.
Добавьте к разметке файла Window1.xaml контейнер TabControl с вкладкой Page1, чтобы файл стал таким<Window x:Class="DataBindingTable.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300" Width="300"
Title="Привязка элементов к БД"
MinHeight="300"
MinWidth="300"
WindowStartupLocation="CenterScreen"
Loaded="Window_Loaded"
>
<Window.Resources>
<SolidColorBrush x:Key="ControlColorBrush"
Color="{x:Static SystemColors.ControlColor}" />
</Window.Resources>
<Grid Background="{StaticResource ResourceKey=ControlColorBrush}">
<TabControl>
<!-- Привязка к таблице Employees через объект -->
<TabItem Header="Page1">
<Grid
Name="gridEmployees1"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees1"
SelectionChanged="listEmployeeslab72_SelectionChanged"
/>
<TextBlock Grid.Row="1" Margin="0,0,5,0">EmployeeID:</TextBlock>
<TextBox Grid.Row="1" Grid.Column="1"
Text="{Binding Path=EmployeeID, Mode=OneWay}"
/>
<TextBlock Grid.Row="2">FullName:</TextBlock>
<TextBox Grid.Row="2" Grid.Column="1"
Text="{Binding Path=FullName, Mode=OneWay}"
/>
<TextBlock Grid.Row="3"></TextBlock>
<TextBox Grid.Row="3" Grid.Column="1"
Text="{Binding Path=Address, Mode=OneWay}"
/>
<TextBlock Grid.Row="4">BirthDate:</TextBlock>
<TextBox Grid.Row="4" Grid.Column="1"
Text="{Binding Path=BirthDate, Mode=OneWay}"
/>
<TextBlock Grid.Row="5">Region:</TextBlock>
<TextBox Grid.Row="5" Grid.Column="1"
Text="{Binding Path=Region, Mode=OneWay}"
/>
</Grid>
</TabItem>
</TabControl>
</Grid>
</Window>
Для отображения данных пользователю мы применили интерфейсные элементы TextBox так будет 'красивше'. Мы явно установили для них тип привязки OneWay, чтобы не было разногласий со свойствами доступа промежуточного объекта Employees1. Свойства оболочки Employees1 у нас работают только для чтения, а элементы TextBox по умолчанию имеют двунаправленный тип привязки TwoWay. Но для отображения данных в привязке можно было бы использовать и элементы TextBlock, тогда тип привязки OneWay указывать было бы необязательно, поскольку у TextBlock иной привязки и не существует - они могут только отображать содержимое.
В файле Window1.xaml вызовите контекстное меню для записей событий Loaded окна и SelectionChanged списка, чтобы создать обработчики командой Navigate to Event Handler
Заполните созданные в файле Window1.xaml.cs обработчики такusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace DataBindingTable
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
Page1();
}
#region Вкладка Page1
Employees1 employees1;
private void Page1()
{
// Загружаем данные и настраиваем источник привязки
employees1 = new Employees1();
listEmployees1.SelectedIndex = 0;
listEmployees1.Focus();
gridEmployees1.DataContext = employees1.GetEmployee(0);
listEmployees1.ItemsSource = employees1.ListFullName;
listEmployees1.DisplayMemberPath = employees1.FullName;
}
private void listEmployeeslab72_SelectionChanged(
object sender, SelectionChangedEventArgs e)
{
gridEmployees1.DataContext =
employees1.GetEmployee(listEmployees1.SelectedIndex);
}
#endregion
}
}
Приведенный код мы упаковали в секцию #region для удобства, чтобы как-то структурировать код отдельных вкладок на тот случай, если их количество станет большим в процессе выполнения упражнения.
Запустите приложение - должен получиться такой результат
При выборе элемента списка текстовые поля заполняются новыми значениями привязанных свойств объекта Employees1. Здесь есть один недостаток: при прямом редактировании текстовых полей значения в них перестают меняться - срабатывает внутренний механизм элемента TextBox. На данном этапе этот недостаток можно устранить, если просто запретить пользователю их редактировать, что согласуется с принятой нами концепцией только отображения данных.
Добавьте в дескрипторы элементов TextBox атрибут Focusable="False" следующим образом<TextBlock Grid.Row="1" Margin="0,0,5,0"<EmployeeID:</TextBlock<
<TextBox Grid.Row="1" Grid.Column="1"
Text="{Binding Path=EmployeeID, Mode=OneWay}"
Focusable="False"
/<
<TextBlock Grid.Row="2"<FullName:</TextBlock<
<TextBox Grid.Row="2" Grid.Column="1"
Text="{Binding Path=FullName, Mode=OneWay}"
Focusable="False"
/<
<TextBlock Grid.Row="3"<Address:</TextBlock<
<TextBox Grid.Row="3" Grid.Column="1"
Text="{Binding Path=Address, Mode=OneWay}"
Focusable="False"
/<
<TextBlock Grid.Row="4"<BirthDate:</TextBlock<
<TextBox Grid.Row="4" Grid.Column="1"
Text="{Binding Path=BirthDate, Mode=OneWay}"
Focusable="False"
/<
<TextBlock Grid.Row="5"<Region:</TextBlock<
<TextBox Grid.Row="5" Grid.Column="1"
Text="{Binding Path=Region, Mode=OneWay}"
Focusable="False"
/<
Запустите приложение, испытайте работу вкладки Page1, разберитесь с кодомВыполним еще одно незначительное действие: поместим уже созданные файлы с процедурным кодом в отдельный каталог.
В панели Solution Explorer вызовите контекстное меню для корневого узла DataBindingTable проекта и командой Add/New Folder добавьте папку с именем Code
В панели Solution Explorer, удерживая клавишу Ctrl, выделите в корне проекта файлы StoreNorthwindDB.cs и Employees1.cs, затем перетащите их мышью в папку Code
Запустите приложение - все продолжает нормально работать, компилятор находит перемещенные файлыВ данном примере задачу вкладки Page1 мы повторим несколько другим способом. Предварительную загрузку данных из хранилища мы поместим не в объект DataTable, а сразу в библиотечную коллекцию List<T> объектов промежуточного класса. И уже к этой коллекции будем привязывать пользовательский интерфейс. Для разнообразия, при заполнении объектов данных коллекции используем ADO.NET -класс OleDbDataReader, поскольку отображаемые данные пока будем только просматривать.
Коллекция объектов-данных List<T> будет служить тонкой оболочкой над извлеченными данными БД и существовать самостоятельно. В нее поместим данные, которые сразу явно преобразуем к нужному типу. Для отображения коллекций данных к ним можно привязывать списковые интерфейсные элементы WPF типа ListBox, ComboBox, ListView, а также элементы для отображения иерархических данных типа Menu, TreeView и др. Такие интерфейсные элементы порождены классом ItemsControl и их еще называют элементами управления элементами

Соответственно, в качестве коллекций, привязываемых к перечисленным элементам управления элементами WPF, можно использовать неотображаемые объекты, наследующие интерфейс System.Collections. ICollection (например, List<T>, ObservableCollection<T>, ArrayList, Array - массив), которые к инфраструктуре WPF могут и не относиться.
В отличие от элементов управления элементами существуют элементы, способные отображать только один объект, например TextBlock или TextBox. Их еще называют элементами управления содержимым (нечем управлять кроме единственного объекта внутри себя).
Как и ранее, вначале создадим 'объект-тонкую оболочку' над извлеченными данными хранилища, общедоступные свойства которого будем привязывать к визуальным элементам пользовательского интерфейса. Поля и приватные свойства класса-оболочки в привязке участвовать не могут. Подход с применение промежуточного класса-оболочки хорош тем, что увеличивает контроль программиста над 'сырыми' данными хранилища. В этом классе мы можем заранее привести данные к нужному типу, скомпоновать и переобъявить их требуемым образом, отсортировать, отфильтровать, сделать какие-то проверки и т.д. Одним словом, - провести предварительную инспекцию и обработку с полным контролем.
Следует заметить, что WPF, как и Windows Forms, тоже способна непосредственно привязывать интерфейсные элементы к извлеченным наборам данных DataTable и DataSet инфраструктуры ADO.NET, без всяких там классов-посредников. В этом мы убедимся чуть позднее, а пока так...
В панели Solution Explorer выделите узел Code и командой Add/Class добавьте в него файл Employee.cs с одноименным классом для хранения приведенных данных, который заполните такusing System;
namespace DataBindingTable
{
// Класс свойств доступа к данным таблицы Employees
public class Employee
{
//********************************************************
// Cвойства доступа для привязки и базовые поля
//********************************************************
int employeeID;
public int EmployeeID
{
get { return employeeID; }
}
string fullName;
public string FullName
{
get { return fullName; }
}
string address;
public string Address
{
get { return address; }
}
string birthDate;
public string BirthDate
{
get { return birthDate; }
}
string region;
public string Region
{
get { return region; }
}
// Конструктор с параметрами
public Employee(int employeeID,
string fullName, string address,
string birthDate, string region)
{
this.employeeID = employeeID;
this.fullName = fullName;
this.address = address;
this.birthDate = birthDate;
this.region = region;
}
// Конструктор по умолчанию
//public Employee() { }
}
}
Класс Employee будет служить тонкой оболочкой для загруженных из хранилища данных. Каждый экземпляр этого класса будет хранить типизированные данные одной считанной из хранилища записи. Конструктор по умолчанию создавать не стали для разнообразия, чтобы подчеркнуть, что использовать будем только параметризованный конструктор.
В файле StoreNorthwindDB.cs добавьте в класс StoreNorthwindDB метод с именем ReadTableEmployees() для заполнения библиотечной коллекции объектов типизированных данных//*********************************************************
// Метод извлечения данных из таблицы Employees
// хранилища базы данных в объект-коллекцию List<T>,
// который называется типизированным списком
//*********************************************************
private List<Employee> employees = null;// Типизированный список
public List<Employee> ReadTableEmployees()
{
// Читаем данные из хранилища только один раз
if (employees != null)
return employees;
// Настраиваем доступ к хранилищу
String cmdText = "SELECT EmployeeID, " +
"(LastName + ', ' + FirstName) AS FullName, " +
"Address, BirthDate, Region FROM Employees";
OleDbConnection conn = new OleDbConnection(connectionString);
OleDbCommand cmd = new OleDbCommand(cmdText, conn);
cmd.CommandType = CommandType.Text;
// Заполняем коллекцию данными из хранилища
employees = new List<Employee>();
try
{
conn.Open();
OleDbDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
// Создать объект Employee, являющийся
// оболочкой для текущей записи таблицы Employees
Employee employee = new Employee(
Convert.ToInt32(reader["EmployeeID"]),
Convert.ToString(reader["FullName"]),
Convert.ToString(reader["Address"]),
Convert.ToString(reader["BirthDate"]),
Convert.ToString(reader["Region"]));
// Добавить в коллекцию
employees.Add(employee);
}
}
catch
{
MessageBox.Show("Ошибка загрузки данных");
}
finally
{
conn.Close(); // Закрываем соединение
}
return employees;
}
Метод ExecuteReader() устанавливает связь с нужной таблицей предварительно открытого соединения с БД в соответствии с настройками SQL -запроса и курсор данных таблицы вначале установлен на первую запись. Метод Read() считывает текущую запись таблицы и перемещает курсор на следующую запись. Цикл чтения записей таблицы продолжается до тех пор, пока курсор данных не установится за последней записью. Тогда метод Read() возвратит false и цикл чтения данных прервется. Пока данные считываются построчно, соединение с БД должно быть открытым. В отличие от метода OleDbDataAdapter.Fill(), соединение с БД при применении класса OleDbDataReader приходится открывать и закрывать вручную. За этим нужно строго следить, иначе данные для других пользователей останутся блокированными.
Добавьте в контейнер TabControl новую вкладку с именем Page2 с привязками интерфейсных элементов к общедоступным свойствам объекта данных Employee<!-- Привязка к таблице Employees через коллекцию объектов -->
<TabItem Header="Page2">
<Grid
Name="gridEmployees2"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees2"
SelectionChanged="listEmployees2_SelectionChanged"
/>
<TextBlock Grid.Row="1" Margin="0,0,5,0">EmployeeID:</TextBlock>
<TextBox Grid.Row="1" Grid.Column="1"
Text="{Binding Path=EmployeeID, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="2">FullName:</TextBlock>
<TextBox Grid.Row="2" Grid.Column="1"
Text="{Binding Path=FullName, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="3">Address:</TextBlock>
<TextBox Grid.Row="3" Grid.Column="1"
Text="{Binding Path=Address, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="4">BirthDate:</TextBlock>
<TextBox Grid.Row="4" Grid.Column="1"
Text="{Binding Path=BirthDate, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="5">Region:</TextBlock>
<TextBox Grid.Row="5" Grid.Column="1"
Text="{Binding Path=Region, Mode=OneWay}"
Focusable="False"
/>
</Grid>
</TabItem>
В выражениях привязки свойства Text элементов TextBox опущено указание источника данных, поэтому WPF будет искать его в свойстве DataContext ближайших родительских элементов. В процедурном коде мы определим источник в свойстве DataContext для элемента gridEmployees2.
Добавьте в класс Window1 файла Window1.xaml.cs следующий блок кода, управляющий привязкой интерфейсных элементов вкладки Page2#region Вкладка Page2
List<Employee> employees2;// Ссылка на типизированный список
private void Page2()
{
// Загружаем данные и настраиваем источник привязки
employees2 = App.StoreNorthwindDB.ReadTableEmployees();
listEmployees2.SelectedIndex = 0;
listEmployees2.Focus();
gridEmployees2.DataContext = employees2[0];
listEmployees2.ItemsSource = employees2;
listEmployees2.DisplayMemberPath = "FullName";
}
private void listEmployees2_SelectionChanged(
object sender, SelectionChangedEventArgs e)
{
gridEmployees2.DataContext =
employees2[listEmployees2.SelectedIndex];
}
#endregion
Здесь мы опять для удобства сгруппировали код в секцию #region. Обработчик события SelectionChanged меняет значение свойства DataContext сетки при выделении элементов списка. Тем самым будет меняться источник, к которому в разметке привязаны элементы TextBox, и соответственно будут меняться значения свойств Text, показывая новые данные.
Поместите в обработчик Window_Loaded() класса Window1 вызов функции Page2()private void Window_Loaded(object sender, RoutedEventArgs e)
{
Page1();
Page2();
}
Запустите приложение и испытайте работу вкладки Page2 - получим точно такую же функциональность, что и для Page1
Разберитесь с кодом рассмотренного примераС последним примером можно немного поэкспериментировать, перенеся привязку списка ListBox из кода в разметку.
Добавьте к разметке элемента ListBox вкладки Page2 в файле Window1.xaml следующую строку привязки<ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees2"
SelectionChanged="listEmployees2_SelectionChanged"
ItemsSource="{Binding}" DisplayMemberPath="FullName"
/>
Модифицируйте в классе Window1 файла Window1.xaml.cs функцию Page2() следующим образомprivate void Page2()
{
// Загружаем данные и назначаем источник привязки
employees2 = App.StoreNorthwindDB.ReadTableEmployees();
listEmployees2.SelectedIndex = 0;
listEmployees2.Focus();
gridEmployees2.DataContext = employees2[0];
//listEmployees2.ItemsSource = employees2;
//listEmployees2.DisplayMemberPath = "FullName";
listEmployees2.DataContext = employees2;
}
В представлении XAML элемента списка свойство ItemsSource мы задали для объекта Binding без указания атрибутов. Поскольку свойство Source объекта Binding не задано, WPF начнет искать источник в свойстве DataContext текущего элемента, а при неудаче - далее будет искать его в родительских элементах вверх по визуальному дереву в направлении его корня. В процедурном коде мы присваиваем свойству DataContext значение объекта-коллекции employees2, тем самым указываем источник связывания в самом элементе списка listEmployees2. Свойство DisplayMemberPath="FullName" списка определяет столбец коллекции-источника, который будет отображаться на экране. В нашем случае - это вычислимый столбец таблицы ADO.NET.
Запустите приложение и испытайте работу вкладки Page2 - ее функциональность осталась прежнейМожно пойти еще дальше в привязке на стороне разметки. Учитывая, что привязанные к элементу ListBox данные синхронно позиционирует свой внутренний курсор построчно в соответсвии с выделенным элементом SelectedItem списка, к этому элементу мы и привяжем свойство Grid.DataContext как к общему источнику данных для текстовых полей TextBox. Получится, что список (элемент управления элементами) привязан к коллекции, а элементы управления содержимым привязаны не к элементам коллекции, а к элементам списка.
Выполните в открывающем дескрипторе элемента Grid вкладки Page2 следующие изменения| Было | <Grid
Name="gridEmployees2"
>
|
|---|---|
| Стало | <Grid
DataContext="{Binding ElementName=listEmployees2, Path=SelectedItem}"
>
|
Удалите, за ненадобностью, из дескриптора элемента ListBox подписку обработчика на событие SelectionChanged| Было | <ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees2"
SelectionChanged="listEmployees2_SelectionChanged"
ItemsSource="{Binding}" DisplayMemberPath="FullName"
/>
|
|---|---|
| Стало | <ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees2"
ItemsSource="{Binding}" DisplayMemberPath="FullName"
/>
|
Удалите полностью, за ненадобностью, из класса Window1 файла Window1.xaml обработчик listEmployees2_SelectionChanged() или закомментируйте содержимое его тела
Закомментируйте в функции Page2() файла Window1.xaml.cs строку кода присвоения свойству DataContext начального элемента коллекции данныхprivate void Page2()
{
// Загружаем данные и назначаем источник привязки
employees2 = App.StoreNorthwindDB.ReadTableEmployees();
listEmployees2.SelectedIndex = 0;
listEmployees2.Focus();
//gridEmployees2.DataContext = employees2[0];
//listEmployees2.ItemsSource = employees2;
//listEmployees2.DisplayMemberPath = "FullName";
listEmployees2.DataContext = employees2;
}
Запустите приложение и испытайте работу вкладки Page2 - она продолжает работать как ни в чем небывалоЧто здесь получилось. Коллекцию Employees объектов Employee, которая инкапсулирует извлеченные из БД данные, мы оставили подключенной к DataContext списка ListBox и ее найдет свойство ItemsSource списка. А вот источником привязки для текстовых полей мы назначили свойство SelectedItem списка, которое ассоциируется с элементом коллекции, и разместили эту информацию в свойстве DataContext сетки Grid. Ее найдут привязанные текстовые поля. Фактически мы сделали то же самое, что выполнял ранее процедурный код в обработчике события SelectionChanged списка.
Вдоль дороги лес густой - с Бабами-Ягами!
А в конце дороги той - плаха с топорами!
(В.С. Высоцкий)
Здесь побочной работы будет мало, поскольку у нас уже есть метод LoadTableEmployees() в классе StoreNorthwindDB, возвращающий заполненный объект DataTable. Только ранее мы его применяли в объекте-оболочке Employees1, а потом этот объект Employees1 использовали в классе Window1 файла Window1.xaml.cs. Теперь метод LoadTableEmployees() вызовем в классе Window1 и возвращенный им объект-коллекцию DataTable используем в привязке того же самого набора интерфейсных элементов WPF.
Есть одна тонкость в намерении применить уже готовый метод LoadTableEmployees(). Мы сгоряча в нем сделали проверку, чтобы объект DataTable создавался только один раз, из благих намерений - желая разгрузить наш компьютер. Но это значит, что к каким бы ссылкам мы этот метод не применяли, все они будут адресовать один и тот же физически существующий объект DataTable. Пока что это не страшно, поскольку извлеченные из таблицы БД данные мы только просматриваем, а не редактируем. И неважно, через какие замочные скважины (вкладки Pages) мы за этими данными будем подглядывать.
Создайте в файле Window1.xaml из копии вкладки Page2 (в ее последней версии после проведенных модификаций) новую вкладку Page3 и сделайте в ней необходимые переименования элементов<!-- Привязка к таблице Employees через объект DataTable из ADO.NET -->
<TabItem Header="Page3">
<Grid
DataContext="{Binding ElementName=listEmployees3, Path=SelectedItem}"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees3"
ItemsSource="{Binding}" DisplayMemberPath="FullName"
/>
<TextBlock Grid.Row="1" Margin="0,0,5,0">EmployeeID:</TextBlock>
<TextBox Grid.Row="1" Grid.Column="1"
Text="{Binding Path=EmployeeID, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="2">FullName:</TextBlock>
<TextBox Grid.Row="2" Grid.Column="1"
Text="{Binding Path=FullName, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="3">Address:</TextBlock>
<TextBox Grid.Row="3" Grid.Column="1"
Text="{Binding Path=Address, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="4">BirthDate:</TextBlock>
<TextBox Grid.Row="4" Grid.Column="1"
Text="{Binding Path=BirthDate, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="5">Region:</TextBlock>
<TextBox Grid.Row="5" Grid.Column="1"
Text="{Binding Path=Region, Mode=OneWay}"
Focusable="False"
/>
</Grid>
</TabItem>
Добавьте в класс Window1 файла Window1.xaml.cs код присоединения коллекции DataTable к интерфейсным элементам вкладки Page3#region Вкладка Page3
private void Page3()
{
// Настраиваем списковый элемент ListBox
listEmployees3.SelectedIndex = 0;
listEmployees3.Focus();
// Назначаем источником объект-коллекцию данных DataTable
listEmployees3.DataContext =
App.StoreNorthwindDB.LoadTableEmployees();
}
#endregion
Опять для удобства мы применили секцию #region.
Поместите в обработчик Window_Loaded() класса Window1 вызов функции Page3() для инициализации списка listEmployees3 вкладки Page3private void Window_Loaded(object sender, RoutedEventArgs e)
{
Page1();
Page2();
Page3();
}
Запустите приложение и испытайте работу вкладки Page3 - получим точно такую же функциональность, что и для двух предыдущих вкладок
Разницы нет между чистою правдой и ложью,
Если вначале и ту и другую раздеть!
(В.С. Высоцкий)
Источником для свойства DataContext можно назначить набор DataSet инфраструктуры ADO.NET, а не DataTable. DataSet считается аналогом реляционной БД, только находится в оперативной памяти. Поскольку объект DataSet способен одновременно инкапсулировать много различных таблиц-объектов DataTable (и воспроизводить отношения между ними), то в выражениях привязки интерфейсных элементов XAML следует делать уточнения, к какой именно таблице набора привязываться. Рассмотрим это на примере вкладки Page4.
Загрузим в набор данных ADO.NET три таблицы сразу, которые в дальнейшем, при необходимости, можно будет использовать для рассмотрения других способов привязки. Но в текущей вкладке Page4 привяжем для отображения только одну таблицу Employees, чтобы глубже почувствовать разницу с предыдущими способами на одинаковом результате. Мы уже неоднократно убеждались в том, что .NET Framework позволяет решать одну и ту же задачу разными способами - поистине мощная библиотека.
Дополните класс StoreNorthwindDB в одноименном файле новым методом ReadDataSet() со следующим содержимым//*********************************************************
// Метод извлечения нетипизированного набора
// данных из хранилища данных Northwind.mdb
//*********************************************************
DataSet ds = null;// Ссылка на нетипизированный набор DataSet
public DataSet ReadDataSet()
{
// Загрузим набор данных только один раз
if (ds != null)
return ds;
ds = new DataSet();// Создаем множественный набор данных
// Заполняем множественный набор данных из БД
using (OleDbConnection conn = new OleDbConnection(connectionString))
{
OleDbCommand selectCommand = conn.CreateCommand();
OleDbDataAdapter adapter = new OleDbDataAdapter(selectCommand);
// Загружаем всю таблицу Employees
selectCommand.CommandText = "SELECT * FROM Employees";
adapter.Fill(ds);// Сам открывает, загружает и закрывает
// Назначаем загруженным данным в наборе такое же имя,
// как и в хранилище, чтобы в дальнейшем не путаться
ds.Tables[0].TableName = "Employees";
// Загружает всю таблицу Customers
selectCommand.CommandText = "SELECT * FROM Customers";
// Назначаем загружаемым данным в наборе имя, в этом способе
// надо переопределить дежурное имя Table до заполнения набора
adapter.TableMappings.Add("Table", "Customers");// Должен стоять перед
adapter.Fill(ds); // Должен стоять после
// Загружает всю таблицу Orders
selectCommand.CommandText = "SELECT * FROM Orders";
// Загружаем и сразу этой порции данных назначаем имя в наборе
adapter.Fill(ds, "Orders");
// Для проверки способов именования в панели Output режима Debug
System.Diagnostics.Debug.WriteLine(ds.Tables[0].TableName);
System.Diagnostics.Debug.WriteLine(ds.Tables[1].TableName);
System.Diagnostics.Debug.WriteLine(ds.Tables[2].TableName);
return ds;
}
}
Чтобы развеять сомнения в том, что таблицы действительно загружены в набор данных и получили заданные имена, в конце метода ReadDataSet() добавлен отладочный код, который работает только для режима Debug. Он выводит результат в панель Output оболочки и его удалять необязательно, потому что в рабочем режиме Release он сам отключится.
Чтобы проверить работу метода, вставьте его вызов в обработчик Window_Loaded() класса Window1 файла Window1.xaml.cs такprivate void Window_Loaded(object sender, RoutedEventArgs e)
{
Page1();
Page2();
Page3();
App.StoreNorthwindDB.ReadDataSet();
}
Запустите проект и проверьте отладочный вывод в панели Output (панель можно открыть командой меню оболочки View/Output )В конце отладочной информации найдем строки
Employees
Customers
Orders
Можно для пробы поменять имена таблиц TableName в наборе данных объектной модели метода и убедиться, что код верно реагирует на эти изменения. При этом строки самих SQL -запросов трогать не следует, поскольку это зона ответственности самого хранилища данных, а не объектной модели ADO.NET.
Теперь осталось сделать вкладку Page4 для размещения интерфейсных элементов отображения данных и привязать к ним заполненный набор данных.
Добавьте в контейнер TabControl файла Window1.xaml копию вкладки Page3 и сделайте очевидные переименования, чтобы разметка новой вкладки Page4 стала такой<!-- Привязка к таблице Employees через объект DataSet из ADO.NET -->
<TabItem Header="Page4">
<Grid
DataContext="{Binding ElementName=listEmployees4, Path=SelectedItem}"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees4"
ItemsSource="{Binding}" DisplayMemberPath="FullName"
/>
<TextBlock Grid.Row="1" Margin="0,0,5,0">EmployeeID:</TextBlock>
<TextBox Grid.Row="1" Grid.Column="1"
Text="{Binding Path=EmployeeID, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="2">FullName:</TextBlock>
<TextBox Grid.Row="2" Grid.Column="1"
Text="{Binding Path=FullName, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="3">Address:</TextBlock>
<TextBox Grid.Row="3" Grid.Column="1"
Text="{Binding Path=Address, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="4">BirthDate:</TextBlock>
<TextBox Grid.Row="4" Grid.Column="1"
Text="{Binding Path=BirthDate, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="5">Region:</TextBlock>
<TextBox Grid.Row="5" Grid.Column="1"
Text="{Binding Path=Region, Mode=OneWay}"
Focusable="False"
/>
</Grid>
</TabItem>
Заметим, что список ListBox имеет имя для того, чтобы в процедурном коде к нему можно было обратиться и присвоить свойству DataContext значение источника привязки - набора данных.
#region Вкладка Page4
private void Page4()
{
// Настраиваем списковый элемент ListBox
listEmployees4.SelectedIndex = 0;
listEmployees4.Focus();
// Назначаем источником набор данных DataSet
listEmployees4.DataContext =
App.StoreNorthwindDB.ReadDataSet();
}
#endregion
Примените в обработчике события Loaded класса Window1 вызов функции Page4()private void Window_Loaded(object sender, RoutedEventArgs e)
{
Page1();
Page2();
Page3();
//App.StoreNorthwindDB.ReadDataSet();
Page4();
}
Измените дескриптор списка вкладки Page4 следующим образом| Было | <ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees4"
ItemsSource="{Binding}" DisplayMemberPath="FullName"
/>
|
|---|---|
| Стало | <ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees4"
ItemsSource="{Binding Path=Employees}" DisplayMemberPath="FullName"
/>
|
Запустите приложение - вкладка Page4 стала работать, но только частично
Текстовые поля TextBox функционируют (кроме поля FullName ) и список тоже, только в нем не отображается информация, установленная в настройках DisplayMemberPath="FullName".
При запущенном приложении загляните в панель Output оболочкиМы видим, что механизм привязки хоть и не возбуждает исключений, но под управлением оболочки все-таки сигнализирует об ошибке, выдавая серию предупреждений, примерно таких
System.Windows.Data Error: 39 : BindingExpression path error:
'FullName' property not found on 'object' ''DataRowView' (HashCode=48835636)'.
BindingExpression:Path=FullName; DataItem='DataRowView' (HashCode=48835636);
target element is 'TextBox' (Name=''); target property is 'Text' (type 'String')
Установите в разметке настроек списка ListBox вкладки Page4 другое значение отображаемого столбца объектной модели источника| Было | <ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees4"
ItemsSource="{Binding Path=Employees}" DisplayMemberPath="FullName"
/>
|
|---|---|
| Стало | <ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Name="listEmployees4"
ItemsSource="{Binding Path=Employees}" DisplayMemberPath="FirstName"
/>
|
Запустите приложение - список стал отображать столбец FirstName, который был загружен из хранилища, а текстовый элемент для поля FullName, которое мы хотели показать, попрежнему остается пустым
Причина тут в том, что физически столбца с именем FullName ни в хранилище, ни в объектной модели набора данных просто не сущестует. Он является вычислимым, то есть формируется налету при извлечении данных. Ранее мы его могли использовать потому, что заранее придумали в выражении SQL -запроса и он попадал в коллекцию источника уже заполненным.
Сейчас вычислимый столбец тоже можно добавить в нужную коллекцию объектной модели набора, но поскольку данные уже заполнены, придется заново перебрать все строки коллекции и присвоить придуманному столбцу требуемыми значениями. А можно поступить по другому: добавить в коллекцию набора ДО ЕГО ЗАПОЛНЕНИЯ новый столбец, настроить его схему требуемым образом, а потом загрузить коллекцию данными из хранилища. Вычислимый столбец сразу будет пересчитан налету и заполнен данными. При этом выражение SQL-запроса менять не придется. Проверим оба способа.
Верните свойство отображаемого столбца списка в прежнее значение DisplayMemberPath="FullName", где опять будет фигурировать имя несуществующего столбца коллекции Employees
Дополните метод ReadDataSet() следующими двумя закомментированными блоками кода, каждый из которых готов создать вычислимый столбец FullName//*********************************************************
// Метод извлечения нетипизированного набора
// данных из хранилища данных Northwind.mdb
//*********************************************************
DataSet ds = null;// Ссылка на нетипизированный набор DataSet
public DataSet ReadDataSet()
{
// Загрузим набор данных только один раз
if (ds != null)
return ds;
ds = new DataSet();// Создаем множественный набор данных
// Заполняем множественный набор данных из БД
using (OleDbConnection conn = new OleDbConnection(connectionString))
{
OleDbCommand selectCommand = conn.CreateCommand();
OleDbDataAdapter adapter = new OleDbDataAdapter(selectCommand);
/*//////////////////////////////////////
// Добавления вычислимого столбца в еще
// незаполненную коллекцию объектной модели
////////////////////////////////////////
// Готовим вычислимый столбец FullName
// для объекта-коллекции Employees
// ДО ЕЕ ЗАПОЛНЕНИЯ В НАБОРЕ ДАННЫХ !!!!!
ds.Tables.Add(new DataTable("Table"));// Дежурное имя по умолчанию
DataColumn column = new DataColumn("FirstName");
column.DataType = typeof(string);
ds.Tables[0].Columns.Add(column);
//
column = new DataColumn();
column.ColumnName = "LastName";
column.DataType = System.Type.GetType("System.String");
ds.Tables[0].Columns.Add(column);
//
column = new DataColumn("FullName", typeof(String));
column.Expression = "LastName + ', ' + FirstName";
ds.Tables[0].Columns.Add(column);
//*/////////////////////////////////////
// Загружаем всю таблицу Employees
selectCommand.CommandText = "SELECT * FROM Employees";
adapter.Fill(ds);// Сам открывает, загружает и закрывает
// Назначаем загруженным данным в наборе такое же имя,
// как и в хранилище, чтобы в дальнейшем не путаться
ds.Tables[0].TableName = "Employees";
// Загружает всю таблицу Customers
selectCommand.CommandText = "SELECT * FROM Customers";
// Назначаем загружаемым данным в наборе имя, в этом способе
// надо переопределить дежурное имя Table до заполнения набора
adapter.TableMappings.Add("Table", "Customers");// Должен стоять перед
adapter.Fill(ds); // Должен стоять после
// Загружает всю таблицу Orders
selectCommand.CommandText = "SELECT * FROM Orders";
// Загружаем и сразу этой порции данных назначаем имя в наборе
adapter.Fill(ds, "Orders");
/*//////////////////////////////////////
// Добавления вычислимого столбца в уже
// заполненную коллекцию объектной модели
////////////////////////////////////////
// Дублируем ссылку для удобства адресации
DataTable tableEmployees = ds.Tables["Employees"];
DataColumn column = new DataColumn("FullName");
column.DataType = typeof(string);
tableEmployees.Columns.Add(column);
// Приведем к верхнему регистру для различения
foreach (DataRow row in tableEmployees.Rows)
row[column] = row["LastName"].ToString().ToUpper() + ", " +
row["FirstName"].ToString().ToUpper();
//*/////////////////////////////////////
return ds;
}
}
Раскомментируйте поочередно один из блоков (но не оба сразу!) создания вычислимого столбца и испытайте работу приложения. Быстрый способ раскомментировать любой из приведенных блоков - добавить слэш перед открывающей скобкой многострочного комментария (перед звездочкой)При реализации любого из рассмотренных способов вкладка Page4 будет выглядеть так, как и ожидалось
В предыдущем примере мы привязывали набор данных к именованному списку, а затем выделенный элемент SelectedItem списка назначали в свойстве DataContext сетки Grid. К этому SelectedItem привязывались текстовые поля TextBox. Объект SelectedItem менялся при выборе пользователя, а следом менялись и значения TextBox. В этом примере набор данных мы присвоим свойству DataContext именованного элемента Grid, чтобы его там нашел и список, и текстовые поля.
Добавьте к интерфейсной коллекции TabControl копию вкладки Page4 и преобразуйте ее во вкладку Page5 так<!-- Прямая привязка элементов к набору данных DataSet -->
<TabItem Header="Page5">
<Grid Name="grid5"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,0,0,3"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ItemsSource="{Binding Path=Employees}"
DisplayMemberPath="FullName"
IsSynchronizedWithCurrentItem="True"
/>
<TextBlock Grid.Row="1" Margin="0,0,5,0">EmployeeID:</TextBlock>
<TextBox Grid.Row="1" Grid.Column="1"
Text="{Binding Path=Employees/EmployeeID, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="2">FullName:</TextBlock>
<TextBox Grid.Row="2" Grid.Column="1"
Text="{Binding Path=Employees/FullName, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="3">Address:</TextBlock>
<TextBox Grid.Row="3" Grid.Column="1"
Text="{Binding Path=Employees/Address, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="4">BirthDate:</TextBlock>
<TextBox Grid.Row="4" Grid.Column="1"
Text="{Binding Path=Employees/BirthDate, Mode=OneWay}"
Focusable="False"
/>
<TextBlock Grid.Row="5">Region:</TextBlock>
<TextBox Grid.Row="5" Grid.Column="1"
Text="{Binding Path=Employees/Region, Mode=OneWay}"
Focusable="False"
/>
</Grid>
</TabItem>
Мы назначили элементу Grid имя для возможности присвоения в процедурном коде его свойству DataContext ссылки на набор данных. Это будет единственный именованный элемент в разметке вкладки Page5, потому что другие элементы мы программировать в процедурном коде не будем. В списке ListBox мы оставили неизменными свойство Path=Employees, означающее таблицу набора в элементе Grid.DataContext, к которой привяжется список, и - отображаемый вычислимый столбец FullName.
Имя списка мы убрали, но добавили атрибут IsSynchronizedWithCurrentItem="True", который заставляет синхронизировать перемещение курсора в коллекции Employees набора данных со сменой выделенного пользователем элемента списка. Привязываемое к набору свойство Text элементов TextBox должно конкретизировать коллекцию в наборе Grid.DataContext вместе с адресуемым свойством привязки.
Добавьте в класс Window1 файла Window1.xaml процедурный код для инициализации источника привязки в именованной сетке Grid следующим образомprivate void Window_Loaded(object sender, RoutedEventArgs e)
{
Page1();
Page2();
Page3();
Page4();
Page5();
}
....................................................
#region Вкладка Page5
private void Page5()
{
// Назначаем источником набор данных DataSet в сетке
grid5.DataContext =
App.StoreNorthwindDB.ReadDataSet();
}
#endregion
Запустите приложение - вкладка Page5 работает
Попробуйте убрать из списка атрибут IsSynchronizedWithCurrentItem="True" или заменить его значение на False - текстовые поля перестают отслеживать выделенные позиции списка: список и коллекция прекратили синхронную работу
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.