При выполнении в лабораторной работе упражнения №1 , а именно при выполнении нижеследующего кода: using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using Microsoft.Xna.Framework.Graphics;
namespace Application1 { public partial class MainForm : Form { // Объявим поле графического устройства для видимости в методах GraphicsDevice device;
public MainForm() { InitializeComponent();
// Подпишемся на событие Load формы this.Load += new EventHandler(MainForm_Load);
// Попишемся на событие FormClosed формы this.FormClosed += new FormClosedEventHandler(MainForm_FormClosed); }
void MainForm_FormClosed(object sender, FormClosedEventArgs e) { // Удаляем (освобождаем) устройство device.Dispose(); // На всякий случай присваиваем ссылке на устройство значение null device = null; }
void MainForm_Load(object sender, EventArgs e) { // Создаем объект представления для настройки графического устройства PresentationParameters presentParams = new PresentationParameters(); // Настраиваем объект представления через его свойства presentParams.IsFullScreen = false; // Включаем оконный режим presentParams.BackBufferCount = 1; // Включаем задний буфер // для двойной буферизации // Переключение переднего и заднего буферов // должно осуществляться с максимальной эффективностью presentParams.SwapEffect = SwapEffect.Discard; // Устанавливаем размеры заднего буфера по клиентской области окна формы presentParams.BackBufferWidth = this.ClientSize.Width; presentParams.BackBufferHeight = this.ClientSize.Height;
// Создадим графическое устройство с заданными настройками device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, DeviceType.Hardware, this.Handle, presentParams); }
protected override void OnPaint(PaintEventArgs e) { device.Clear(Microsoft.Xna.Framework.Graphics.Color.CornflowerBlue);
base.OnPaint(e); } } } Выбрасывается исключение: Невозможно загрузить файл или сборку "Microsoft.Xna.Framework, Version=3.0.0.0, Culture=neutral, PublicKeyToken=6d5c3888ef60e27d" или один из зависимых от них компонентов. Не удается найти указанный файл. Делаю все пунктуально. В чем может быть проблема? |
Работа с потоками данных
Пример 4. Создание сервера БД как Console Application
Хоть консольное окно и будет считаться пользовательским интерфейсом, но для простоты мы его никак не станем задействовать.
- Добавьте к решению командой File/Add/New Project консольное приложение с именем ConsoleServerDB
- В панели Solution Explorer в корень нового проекта скопируйте из предыдущего проекта PicturesServerDB файл App.config и папку Data. На предложение мастера создать типизированный DataSet скажите Cancel
- В панели Solution Explorer через контекстное меню для узла References добавьте к проекту ссылку на System.Configuration.dll
- Откройте файл Program.cs проекта консольного приложения и наполните его следующим кодом (приводится полностью)
using System; using System.Collections.Generic; using System.Text; // Дополнительные пространства имен для ADO.NET using System.Data; using System.Data.OleDb; using System.Data.Common; // Дополнительные пространства имен using System.IO; using System.Net; using System.Net.Sockets; using System.Threading; using System.Collections; using System.Configuration; namespace ConsoleServerDB { class Program { int port = 12000; String hostName = "127.0.0.1"; // local IPAddress localAddr; TcpListener server = null; // Ссылка на сервер String separator = "#"; // Разделитель имен в строке ответа String connectionString; // Строка соединения с БД // Добавили конструктор public Program() { // Извлекаем в поле строку соединения с БД из файла App.config connectionString = System.Configuration.ConfigurationManager. ConnectionStrings["PicturesDB"].ConnectionString; // Конвертируем IP в другой формат localAddr = IPAddress.Parse(hostName); // Запускаем в новом потоке (ните) Thread thread = new Thread(ExecuteLoop); thread.IsBackground = true; thread.Start(); } static void Main(string[] args) { Console.WindowWidth = 20; Console.WindowHeight = 5; new Program(); Console.ReadLine();// Держим основной поток (нить) приложения } private void ExecuteLoop() { try { server = new TcpListener(localAddr, port);// Создаем сервер-слушатель server.Start();// Запускаем сервер // Бесконечный цикл прослушивания клиентов while (true) { // Проверяем очередь соединений if (!server.Pending())// Очередь запросов пуста continue; TcpClient client = server.AcceptTcpClient();// Текущий клиент // Создаем потоки сетевых соединений StreamReader readerStream = new StreamReader(client.GetStream()); NetworkStream streamOut = client.GetStream(); StreamWriter writerStream = new StreamWriter(streamOut); // Читаем команду клиента String receiverData = readerStream.ReadLine(); // Распознаем и исполняем switch (receiverData) { case "!!!GetNames!!!":// Посылаем имена, разделенные сепаратором String names = GetNames(); writerStream.WriteLine(names); // Используем через оболочку writerStream.Flush(); break; default:// Посылаем рисунок Byte[] bytes = GetPicture(receiverData); streamOut.Write(bytes, 0, bytes.Length);// Используем напрямую streamOut.Flush(); break; } // Закрываем соединение и потоки, порядок неважен client.Close(); readerStream.Close(); writerStream.Close(); } } finally { // Останавливаем сервер server.Stop(); } } // Извлечение из БД имен рисунков и упаковка их в одну строку для пересылки клиенту string GetNames() { // Создаем и настраиваем инфраструктуру ADO.NET OleDbConnection conn = new OleDbConnection(connectionString); OleDbCommand cmd = new OleDbCommand("SELECT FileName FROM MyTable"); cmd.Connection = conn; conn.Open(); // Извлекаем имена рисунков OleDbDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection); // Формируем строку исходящих данных StringBuilder sb = new StringBuilder(); foreach (DbDataRecord record in reader)// Равносильно чтению reader.Read() sb.Append(((string)record["FileName"]).Trim() + separator); // Соединение здесь закроет сам объект DataReader после прочтения всех данных // в соответствии с соглашением при его создании CommandBehavior.CloseConnection // Удаляем лишний последний символ сепаратора sb.Replace(separator, String.Empty, sb.ToString(). LastIndexOf(separator), separator.Length); return sb.ToString(); } // Извлечение из БД самого рисунка для отправки клиенту byte[] GetPicture(String name) { // Создаем и настраиваем инфраструктуру ADO.NET OleDbConnection conn = new OleDbConnection(); conn.ConnectionString = connectionString; // Создаем и настраиваем объект команды, параметризованной по имени рисунка OleDbCommand cmd = new OleDbCommand(); cmd.Connection = conn; cmd.CommandType = CommandType.Text; // Необязательно! Установлено по умолчанию cmd.CommandText = "SELECT Picture FROM MyTable WHERE FileName=?"; cmd.Parameters.Add(new OleDbParameter()); cmd.Parameters[0].Value = name;// Имя рисунка OleDbDataAdapter adapter = new OleDbDataAdapter(cmd); // Извлекаем рисунок из БД DataTable table = new DataTable(); adapter.Fill(table); byte[] bytes = (byte[])table.Rows[0]["Picture"]; // Подключаемся к рисунку return bytes; } } }
Приведенный код в точности соответствует серверу, созданному нами ранее как сервис, только дополнительно добавилась точка входа Main() и класс стал называться по другому.
- Откомпилируйте проект командой Build/Build Solution
- Через Проводник Windows зайдите в каталоги bin/Debug проектов ConsoleServerDB и PicturesClientDB
- Для удобства запуска создайте для исполнимых сборок ConsoleServerDB.exe и PicturesClientDB.exe ярлыки на рабочем столе компьютера, попробуйте запустить несколько копий каждого из приложений
Мы видим, что копии клиентских приложений запускаются нормально, а для консольного сервера дублирующие копии вылетают с выбросом исключения операционной системы. А если запустить ранее созданный сервисный сервер (в Службах Windows ), то консольный сервер будет вылетать даже при первом запуске.
- Поэкспериментируйте с построенными приложениями, осмыслите происходящее и разберитесь с кодом
Пример 5. Создание сервера БД как Windows Forms
Чтобы показать, что сервером может выступать приложение любого типа, повторим предыдущую задачу на приложении с оконным интерфейсом. Учитывая, что в сети прослушивать клиентов по одному и тому же адресу может только один запущенный экземпляр сервера, заодно покажем, как блокировать повторный его запуск. Нового здесь будет мало, только создадим оконную заготовку приложения Windows Forms и просто скопируем код сервера в это приложение, изменив самую малость. А потом еще добавим в файл Program.cs код блокирования повторного запуска, - вот и все.
Создание оконного сервера
- Добавьте к проекту командой File/Add/New Project оболочки новый проект с именем WindowServerDB (стартовым его не делайте)
- В панели Solution Explorer вызовите для узла WindowServerDB контекстное меню и выполните команду Build
- В Проводнике Windows зайдите в каталог исполнимой сборки проекта bin\Debug и отправьте ярлык исполнимой сборки WindowServerDB.exe на рабочий стол контекстной командой Отправить
- Заполните файл Form1.cs следующим кодом (приводится полностью и почти в точности повторяет код файла Program.cs проекта ConsoleServerDB )
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; // Дополнительные пространства имен для ADO.NET using System.Data.OleDb; using System.Data.Common; // Дополнительные пространства имен using System.IO; using System.Net; using System.Net.Sockets; using System.Threading; using System.Collections; using System.Configuration; namespace WindowServerDB { public partial class Form1 : Form { int port = 12000; String hostName = "127.0.0.1"; // local IPAddress localAddr; TcpListener server = null; // Ссылка на сервер String separator = "#"; // Разделитель имен в строке ответа String connectionString; // Строка соединения с БД public Form1() { InitializeComponent(); // Извлекаем в поле строку соединения с БД из файла App.config connectionString = System.Configuration.ConfigurationManager. ConnectionStrings["PicturesDB"].ConnectionString; // Конвертируем IP в другой формат localAddr = IPAddress.Parse(hostName); // Запускаем в новом потоке (ните) Thread thread = new Thread(ExecuteLoop); thread.IsBackground = true; thread.Start(); } private void ExecuteLoop() { try { server = new TcpListener(localAddr, port);// Создаем сервер-слушатель server.Start();// Запускаем сервер // Бесконечный цикл прослушивания клиентов while (true) { // Проверяем очередь соединений if (!server.Pending())// Очередь запросов пуста continue; TcpClient client = server.AcceptTcpClient();// Текущий клиент // Создаем потоки сетевых соединений StreamReader readerStream = new StreamReader(client.GetStream()); NetworkStream streamOut = client.GetStream(); StreamWriter writerStream = new StreamWriter(streamOut); // Читаем команду клиента String receiverData = readerStream.ReadLine(); // Распознаем и исполняем switch (receiverData) { case "!!!GetNames!!!":// Посылаем имена, разделенные сепаратором String names = GetNames(); writerStream.WriteLine(names); // Используем через оболочку writerStream.Flush(); break; default:// Посылаем рисунок Byte[] bytes = GetPicture(receiverData); streamOut.Write(bytes, 0, bytes.Length);// Используем напрямую streamOut.Flush(); break; } // Закрываем соединение и потоки, порядок неважен client.Close(); readerStream.Close(); writerStream.Close(); } } finally { // Останавливаем сервер server.Stop(); } } // Извлечение из БД имен рисунков и упаковка их в одну строку для пересылки клиенту string GetNames() { // Создаем и настраиваем инфраструктуру ADO.NET OleDbConnection conn = new OleDbConnection(connectionString); OleDbCommand cmd = new OleDbCommand("SELECT FileName FROM MyTable"); cmd.Connection = conn; conn.Open(); // Извлекаем имена рисунков OleDbDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection); // Формируем строку исходящих данных StringBuilder sb = new StringBuilder(); foreach (DbDataRecord record in reader)// Равносильно чтению reader.Read() sb.Append(((string)record["FileName"]).Trim() + separator); // Соединение здесь закроет сам объект DataReader после прочтения всех данных // в соответствии с соглашением при его создании CommandBehavior.CloseConnection // Удаляем лишний последний символ сепаратора sb.Replace(separator, String.Empty, sb.ToString(). LastIndexOf(separator), separator.Length); return sb.ToString(); } // Извлечение из БД самого рисунка для отправки клиенту byte[] GetPicture(String name) { // Создаем и настраиваем инфраструктуру ADO.NET OleDbConnection conn = new OleDbConnection(); conn.ConnectionString = connectionString; // Создаем и настраиваем объект команды, параметризованной по имени рисунка OleDbCommand cmd = new OleDbCommand(); cmd.Connection = conn; cmd.CommandType = CommandType.Text; // Необязательно! Установлено по умолчанию cmd.CommandText = "SELECT Picture FROM MyTable WHERE FileName=?"; cmd.Parameters.Add(new OleDbParameter()); cmd.Parameters[0].Value = name;// Имя рисунка OleDbDataAdapter adapter = new OleDbDataAdapter(cmd); // Извлекаем рисунок из БД DataTable table = new DataTable(); adapter.Fill(table); byte[] bytes = (byte[])table.Rows[0]["Picture"]; // Подключаемся к рисунку return bytes; } } }
- В панели Solution Explorer раскройте узел проекта ConsoleServerDB и, удерживая клавишу Ctrl, выделите папку Data и файл App.config, затем перетащите их курсором в корень проекта WindowServerDB. На предложение мастера создать набор данных скажите Cancel
-
Убедитесь, чтобы скопированная база данных Data/Pictures.my2.mdb имела в панели Properties значение:
- Copy to Output Directory=Copy if newer
- В панели Solution Explorer выделите любой элемент проекта WindowServerDB (например, корень), откройте через меню оболочки панель View/Object Browser, задайте в ней режим Browse: All Components, а в поле поиска - пространство имен System.Configuration. Для найденного пространства имен выделите родительскую сборку System.configuration.dll и кнопкой с пиктограммой Add to References in Selected Project in Solution Explorer добавьте ссылку в папку References текущего проекта (таким способом можно добавлять сразу несколько ссылок) для видимости компилятором класса ConfigurationManager
- Откомпилируйте проект командой Rebuild и запустите этот оконный сервер через ярлык рабочего стола компьютера - сервер хоть и с пустым окном, но работает
- Проверьте работу сервера WindowServerDB.exe совместно с клиентской сборкой PicturesClientDB.exe - все должно работать исправно!
Блокирование дублирующего запуска оконного сервера
Не очень хорошо, когда пользователь случайно попытается запустить второй экземпляр оконного сервера и получит системное исключение (попробуйте!). Хоть это ни к чему страшному не приведет, но может вызвать замешательство (или даже инфаркт с миокардом и кариесом) у бедного пользователя, поэтому пощадим миокард и заблокируем эту возможность.
Не всегда у управляемой среды исполнения CLR хватает своих средств для решения любых задач и в некоторых случаях приходится напрямую обращаться к библиотекам и функциям операционной системы. Для таких целей предусмотрен атрибут DllImport, с помощью которого можно вызвать любую функцию API Windows. Это относится и к нашей задаче - когда требуется блокировать повторный запуск приложения, то можно воспользоваться таблицей глобальных атомов самой операционной системы. А к ней из управляемого кода C# доступ невозможен.
- Измените код файла Program.cs проекта WindowServerDB на следующий (приводится полностью)
using System; using System.Collections.Generic; using System.Windows.Forms; // Дополнительное пространство имен импорта типов API using System.Runtime.InteropServices; namespace WindowServerDB { static class Program { [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] static extern ushort GlobalAddAtom(string lpString); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] static extern ushort GlobalFindAtom(string lpString); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] static extern ushort GlobalDeleteAtom(ushort atom); [STAThread] static void Main() { string strAtom = Application.ProductName + Application.ProductVersion; ushort atom = GlobalFindAtom(strAtom); if (atom != 0) { MessageBox.Show("Прежде чем создать новый\n" + "экземпляр этого приложения,\n" + "сначала следует закрыть текущий!"); } else { // Атом не найден в таблице атомов, создаем его atom = GlobalAddAtom(strAtom); // Настройка приложения Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); try { // Рабочий цикл Windows Application.Run(new Form1()); } finally { // Удаляем атом в любом случае GlobalDeleteAtom(atom); } } } } }
Вопросы взаимодействия CLR с операционной системой, потоки выполнения Thread, атрибуты и многое другое прекрасно описаны в:
Дубовцев А.В. Microsoft .NET в подлиннике. - СПб.: БХВ-Петербург, 2004. - 704 с.
- Запустите оконный сервер WindowServerDB и убедитесь, что дублирующий запуск приложения теперь заблокирован
Сокрытие окна сервера
Сейчас нам стыдно смотреть на окно сервера - оно просто пустое. В дальнейшем мы можем его задействовать. Например, для регистрации администратором клиентов, редактирования настроек сервера и другими задачами, связанными с нашим приложением. А пока для тренировки его отображение на экране можно просто отключить: оно есть и работает, просто его нигде не видно (почти нигде). Для этого воспользуемся событием Shown объекта окна.
- Дополните класс формы в файле Form1.cs следующим кодом
namespace WindowServerDB { public partial class Form1 : Form { ................................................ public Form1() { ................................................ // Подписываемся на событие для сокрытия окна this.Shown += Form1_Shown; } void Form1_Shown(object sender, EventArgs e) { this.Hide(); } ................................................ } }
- Откомпилируйте сервер WindowServerDB и запустите через ярлыки его и клиента PicturesClientDB - все работает, но сам сервер невидно и как его остановить!
- Вызовите жестом Ctrl-Alt-Del Диспетчер задач Windows, откройте вкладку Процессы - сервер работает и его можно даже закрыть
Замечания
Если сейчас закрыть невидимый сервер через Диспетчер задач Windows, то он повторно не запустится - сработает наша блокировка. Причина в том, что в данном случае приложение завершится нестандартно, а именно: не через среду CLR, а напрямую через Диспетчер задач Windows. Соответственно, и атом (наша метка блокировки) в таблице глобальных атомов останется. Он будет удален только при перезагрузке Windows. Здесь нужно выбирать что-то одно: либо оставить блокировку и не скрывать сервер, либо убрать блокировку и сделать сервер невидимым для пользователя.
Теперь мы знаем, что необязательно делать сервер как Службу Windows ( Windows Service ), чтобы скрыть его от пользователя. Но можно сделать сервер как приложение Windows Forms, пусть даже с пустой формой, скрыть его и включить в список программ, автоматически запускаемых при загрузке Windows. Это уже дело вкуса - наша задача состояла в том, чтобы проверить такую возможность.
Кстати, консольное приложение не имеет события, подобного событию Shown объекта окна. Оно показывается самой CLR при запуске и скрыть его программно невозможно, хотя мы его и применяли при создании сервера.
Есть еще один вариант: установить состояние окна при запуске свернутым, тогда его можно завершить через среду CLR естественным образом.
- Внесите в конструктор формы следующие очевидные изменения
namespace WindowServerDB { public partial class Form1 : Form { ................................................ public Form1() { ................................................ // Подписываемся на событие для сокрытия окна //this.Shown += Form1_Shown; // Минимизируем окно при запуске this.WindowState = FormWindowState.Minimized; } void Form1_Shown(object sender, EventArgs e) { this.Hide(); } ................................................ } }
- Запустите сервер - теперь и блокировка работает, и сервер можно закрыть, и миокард целый!