Клиент-серверная работа с табличными данными для начинающих

в 10:35, , рубрики: php, windows forms, клиент-сервер, таблицы, метки: , , , ,
Вместо начала.

Недавно пришлось заняться написанием приложения по работе. Раньше работал исключительно с 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
  • Email
  • 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js