Вместо начала.
Недавно пришлось заняться написанием приложения по работе. Раньше работал исключительно с PHP и web-мордами, однако быо требование сделать полноценное windows-приложение с авторизацией, использованием forms и прочей «петрушки». Эту статью я пишу на отвлеченном абстрактном примере с целью сделать ман доступным и простым. Собственно, здесь важен сам ход действий, нежели само приложение.
Задача была без веб-интерфейса работать с табличными данными, получаемыми с сервера. Доступные инструменты: web-сервер Apache + PHP + MySQL и C#-приложение на стороне клиента.
Профессионалам вряд ли будет интересно. А вот новичкам, мне кажется, может пригодиться. Очень надеюсь, что я не перемудрил с воплощением идеи.
Кому интересна реализация связки — прошу под кат.
Для начала опишу часть кода, которая обеспечивает обмен данными с сервером, а также работу со строками в отправляемых и получаемых сообщениях.
У меня это реализовано в готовых классах, но здесь я приведу лишь нужные методы, а не целиком листинги всех классов.
Клиент, напомню, пишется на языке C#. От WPF пришлось отказаться, т. к. в нашей конторе больше жалуют стандартный вариант с Windows Forms. Серверная часть создавалась на PHP, ибо это просто оказалось быстрее, чем писать полноценный CGI-клиент под Mono Complete, хотя в будущем планируется отказываться от
Хочу сказать, что это мой первый опыт работы с VisualStudio (использую VS2013 c ReSharper) и C#. Потому если где-то можно было сделать удобнее, не судите строго — с удовольствием почитаю комментарии и учту в дальнейших своих разработках.
Шифрование данных
За основу взят формат Base-64 из-за своей универсальности на различных платформах.
// Преобразуем строку в Base64 формат public static string Base64Encode(string inputString) { return Convert.ToBase64String(Encoding.UTF8.GetBytes(inputString)); }
// Получаем декодированную из Base64 строку public static string Base64Decode(string inputString) { return Encoding.UTF8.GetString(Convert.FromBase64String(inputString)); }
Запрос к серверу
Для работы потребуется библиотека "Newtonsoft.Json". Добавьте ее в References проекта, иначе не будет работать. Сразу хочу отметить, что сервер работает под управлением Debian Squeeze и везде используется кодировка UTF-8. Потому в коде предусмотрены строки для совместимости. Никому не хочется кракозябр в гуе. Комментировать такой код по ходу не вижу смысла — он очевиден.
public static Dictionary<string, string> Response(string handlerString, string inputString = "") { try { // формируем строку запроса var requestString = (inputString.Length > 0) ? "handler=" + handlerString + "&args=" + Base64Encode(inputString) : "handler=" + handlerString; // Для обмена данными с сервером используем HttpWebRequest. Думаю, нет смысла его детально комментировать - есть поисковики, чтоб найти маны по работе с ним. // * В разделе Properties.Settings.Default заранее прописаны некоторые параметры, которые можно было бы написать нативно в коде, но так лучше, IMHO. Так что в дальнейшем будет указываться именно такой формат, а в комментариях к коду прописаны мои значения. // Properties.Settings.Default.handlerUri = (string) "http://ваш_интернет_домен/handler.php" var request = (HttpWebRequest) WebRequest.Create(Properties.Settings.Default.handlerUri); // Properties.Settings.Default.httpMethod = (string) "POST" request.Method = Properties.Settings.Default.httpMethod; request.Credentials = CredentialCache.DefaultCredentials; var encoding = new UTF8Encoding(); var bytes = encoding.GetBytes(requestString); // Properties.Settings.Default.httpContentType = (string) "application/x-www-form-urlencoded" request.ContentType = Properties.Settings.Default.httpContentType; request.ContentLength = bytes.Length; using (var newStream = request.GetRequestStream()) { newStream.Write(bytes, 0, bytes.Length); newStream.Close(); } var response = (HttpWebResponse) request.GetResponse(); var streamReader = new StreamReader(response.GetResponseStream(), Encoding.UTF8); var responseString = streamReader.ReadToEnd().Trim(); // ответ преобразовываем в JSON-формат, предварительно декодировав строку из Base-64 в адекватный вид. И... у меня прилетали лишние кавычки, потому пришлось сделать костыль в виде .Trim('"'). По крайней мере, работает без глюков теперь. return JsonConvert.DeserializeObject<Dictionary<string, string>>( Base64Decode(responseString.Trim('"'))); } catch (Exception exception) { // Сделаем пользователю оповещение о том, что на сервере есть какая-то проблема с обработчиком. // Properties.Settings.Default.errorMessageServerHandlerBug = (string) "Возможно, на сервере отсутствует обработчик запросов приложения. Проверьте правильность инсталяции или обратитесь к системному администратору.". // Properties.Settings.Default.msgboxCaptionError = (string) "Ошибка". MessageBox.Show( Properties.Settings.Default.errorMessageServerHandlerBug + Convert.ToChar("\n") + exception.Message, Properties.Settings.Default.msgboxCaptionError, MessageBoxButtons.OK, MessageBoxIcon.Error); // Выход из приложения осуществляется исходя из логики, что если нет обработчика - нет смысла в запуске программы, т.к. начиная с авторизации она уже не будет работать. Application.Exit(); // несмотря на завершение приложения, VS ругается на отсутствие return'а, потому не стал убирать. Честно говоря, сомневаюсь, что исполнение программы дойдет до этой строки :) return JsonConvert.DeserializeObject<Dictionary<string, string>>("{\"type\":\"error\",\"data\":\"-1\"}"); } }
Теперь перейдем к основной теме статьи.
Давайте рассмотрим на примере
Поля (заголовки столбцов) таблицы пусть будут следующими:
- Id
- Title
- PhoneNumber
- Messengers
Для начала опишу часть на PHP (серверная сторона).
define("DB_HOST", "localhost"); define("DB_USER", "имя_пользователя_БД"); define("DB_PASS", "пароль_пользователя_БД"); define("DB_NAME", "имя_нужной_базы_данных"); define("DB_CHARSET", "utf8"); define("ANSWER_SUCCESS", "success"); define("ANSWER_ERROR", "error"); function make_answer($type, $data) { print base64_encode(json_encode([ "type" => $type, "data" => $data ])); exit();// обязательно завершаем выполнение всех дальнейших действий сервера. } $db = new PDO("mysql:host=".DB_HOST.";dbname=".DB_NAME, DB_USER, DB_PASS); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->exec("SET NAMES '".DB_CHARSET."'"); $query = $db->query("SELECT `Id`,`Title`,`PhoneNumber`,`Email`,`Messengers` FROM `contacts` WHERE 1"); $query->execute(); $query->setFetchMode(PDO::FETCH_ASSOC); $result = []; while ($current = $query->fetch()) { $result[] = [ "Id" => base64_encode($current["Id"]), "Title" => base64_encode($current["Title"]), "PhoneNumber" => base64_encode($current["PhoneNumber"]), "Email" => base64_encode($current["Email"]), "Messengers" => base64_encode($current["Messengers"]) ]; } if (count($result) > 0) { make_answer(ANSWER_SUCCESS, $result); } make_answer(ANSWER_ERROR, "Записей не найдено");
В общих чертах. Здесь мы подключаемся к базе данных, получаем нужные данные (используется PDO) и формируем ответ в виде массива. Если записи отсутствуют, возвращаем сообщение об ошибке.
Массив с данными имеет формат:
Array[
0 => array[«Id» => «значение», «Title» => «значение», «PhoneNumber» => «значение», «Email» => «значение», «Messengers» => «значение»],
1 => array[«Id» => «значение», «Title» => «значение», «PhoneNumber» => «значение», «Email» => «значение», «Messengers» => «значение»],
…
N => array[«Id» => «значение», «Title» => «значение», «PhoneNumber» => «значение», «Email» => «значение», «Messengers» => «значение»]
]
В дальнейшем считаю такую форму наиболее удобной для работы с базами данных и наборами двумерных данных вида «ключ — значение».
Обмен данных происходит по протоколу HTTP, потому достаточно обычного вывода «на экран» полученных данных и завершения работы скрипта.
Значение каждого поля кодируется в Base-64 формат, а весь массив данных преобразуется в JSON-формат и снова кодируется в Base-64. Достаточно безопасно и удобно для разбора в клиентской части приложения.
Теперь разберем сторону клиента.
Я использовал сочетание компонентов dataSet и dataGridView.
На форме есть два (для начала) компонента: dataSetContactsTable, dataGridViewContactsTable.
Советую сразу давать всем компонентам понятные имена — иначе потом такая каша в коде начнется, что проще написать софтину заново, нежели ее отрефакторить.
При добавлении компонента dataSet я выбрал вариант «Нетипизированный набор данных».
Теперь настроим компонент dataSet. В свойствах я установил ему:
- Name = dataSetContactsTable
- DataSetName = DataSetContactsTable
- Tables = (см.далее по тексту, что создавалось)
Жмем кнопочку «Добавить» и настраиваем созданную таблицу. Вот те параметры, которые я установил:
- Columns = (опишу колонки чуть ниже)
- TableName = TableContactsView
- Name = dataTableContactsView
- Modifiers = Public
Колонки таблицы должны соответствовать прилетающим с сервера данным. Потому, было сделано так:
- (ID — Caption — ColumnName — ReadOnly — Name)
- 0 — ID — ColumnId — true — dataColumnId
- 1 — Контакт — ColumnTitle — true — dataColumnTitle
- 2 — Телефон — ColumnPhoneNumber — true — dataColumnPhoneNumber
- 3 — E-mail — ColumnEmail — true — dataColumnEmail
- 4 — Говорилки — ColumnMessengers — true — dataColumnMessengers
Может возникнуть вопрос, почему все колонки установлены в режим только для чтения. Это связано с особенностями софтинки, когда по дабл-клику на строке должно вызываться отдельное окно, в котором проводится работа с выделенной записью.
Переходим к настройке компонента dataGrid. И сначала опишу те изменения в свойствах, которые я сделал для него.
Видимо, не имеет смысла описывать дизайнерскую часть конфига — у каждого она может быть своя. Коснусь только вопроса колонок и привязки к компоненту dataSet.
- DataMember = TableContactsView
- DataSource = dataSetContactsTable
- Columns = (см.ниже по тексту)
Все поля таблицы, кроме ID записи, должны отображаться пользователю приложения. Потому поле Id — единственное, у которого установлен параметр «Visible» в значение «False». В остальном настройки похожи.
- HeaderText — DataPropertyName
- Id — ColumnUserId
- Контакт — ColumnTitle
- Телефон — ColumnPhoneNumber
- E-mail — ColumnEmail
- Говорилки — ColumnMessengers
Всё. Работа мышкой завершена, теперь можно перейти к коду.
private void FormContacts_Load(object sender, EventArgs e) { LoadContactsTable(); }
Для дальнейшей работы сразу создадим класс, который поможет нам в разборе JSON-массива, прилетающего с сервера.
public class ContactsList { public string Id { get; set; } public string Title { get; set; } public string PhoneNumber { get; set; } public string Email { get; set; } public string Messengers { get; set; } }
Теперь необходимо создать метод, очищающий и заново загружающий данные в таблицу на форме (он уже упоминался выше в коде FormContacts_Load).
// строка с аргументами предусмотрена на случай, если будут запрашиваться данные, основанные на каком-нибудь фильтре. Именно параметры фильтра и указываются в аргументах. public void LoadContactsTable(string args = "") { // Очищаем таблицу предварительно, т.к. надо заполнить ее свежими данными с сервера // Советую реализовать это иначе, если данных окажется много - не зачем понапрасну нагружать сеть. dataTableContactsView.Clear(); // получаем данные с сервера var contactsResponse = Response("getContactsTable", args); // в случае, если они есть - будем их обрабатывать и заполнять таблицу if (contactsResponse["type"].Trim() == "success") { // Создадим список на основе класса ContactsList, созданного ранее var contactsJsonList = (List<ContactsList>) JsonConvert.DeserializeObject(contactsResponse["data"].Trim(), typeof (List<ContactsList>)); foreach (var key in contactsJsonList) { // здесь заполняется текущая строка таблицы var row = dataTableContactsView.NewRow(); row[0] = Base64Decode(key.Id); row[1] = Base64Decode(key.Title); row[2] = Base64Decode(key.PhoneNumber); row[3] = Base64Decode(key.Email); row[4] = Base64Decode(key.Messengers); dataTableContactsView.Rows.Add(row); } } // если данные отсутствуют, выведем пользователю сообщение, что их нет else { MessageBox.Show(contactsResponse["data"].Trim(), @"Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); } }
Вот, в принципе, и всё. При всех удачных условиях (есть сервер, написан обработчик для запроса, есть ответ от сервера, есть данные в базе) на форме отобразятся данные, которые нам необходимы.
По аналогичному принципу можно построить любое другое табличное приложение. Вроде, всё. Надеюсь, что ничего не забыл описать и что материал описан достаточно доступно и просто.
Спасибо за внимание.

