Вместо начала.
Недавно пришлось заняться написанием приложения по работе. Раньше работал исключительно с PHP и web-мордами, однако быо требование сделать полноценное windows-приложение с авторизацией, использованием forms и прочей «петрушки». Эту статью я пишу на отвлеченном абстрактном примере с целью сделать ман доступным и простым. Собственно, здесь важен сам ход действий, нежели само приложение.
Задача была без веб-интерфейса работать с табличными данными, получаемыми с сервера. Доступные инструменты: web-сервер Apache + PHP + MySQL и C#-приложение на стороне клиента.
Профессионалам вряд ли будет интересно. А вот новичкам, мне кажется, может пригодиться. Очень надеюсь, что я не перемудрил с воплощением идеи.
Кому интересна реализация связки — прошу под кат.
Для начала опишу часть кода, которая обеспечивает обмен данными с сервером, а также работу со строками в отправляемых и получаемых сообщениях.
У меня это реализовано в готовых классах, но здесь я приведу лишь нужные методы, а не целиком листинги всех классов.
Клиент, напомню, пишется на языке C#. От WPF пришлось отказаться, т. к. в нашей конторе больше жалуют стандартный вариант с Windows Forms. Серверная часть создавалась на PHP, ибо это просто оказалось быстрее, чем писать полноценный CGI-клиент под Mono Complete, хотя в будущем планируется отказываться от «пыхи» PHP в пользу писанного на C# демона.
Хочу сказать, что это мой первый опыт работы с 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);
}
}
Вот, в принципе, и всё. При всех удачных условиях (есть сервер, написан обработчик для запроса, есть ответ от сервера, есть данные в базе) на форме отобразятся данные, которые нам необходимы.
По аналогичному принципу можно построить любое другое табличное приложение. Вроде, всё. Надеюсь, что ничего не забыл описать и что материал описан достаточно доступно и просто.
Спасибо за внимание.
Автор: SindyJay