Разработать клиент серверное приложение для работы с произвольными информационными карточками. Информационная карточка состоит из 2 элементов информации: названия карточки и графического изображения. В качестве вариантов информации можно использовать следующие типы: товары в магазине, книги, мобильные телефоны и т.д.
Серверное приложение должно работать по протоколу HTTP REST и использовать .NET Core. Серверное приложение представляет из себя службу Windows или обычное Windows приложение, предоставляющее HTTP API для клиентского приложения. Клиентская сторона должна представлять из себя Windows приложение с использованием технологии построения графических интерфейсов WPF.
- Загрузка информационных карточек из файла на стороне сервера. Формат файла на выбор – JSON, XML
- Сохранение добавленных и изменённых информационных карточек в файл на стороне сервера
- Обработка запросов пользовательского GUI на CRUD операции с информационными карточками
- Обработка ошибок при работе с клиентом
- Показ списка информационных карточек в GUI клиентского приложения с отображением графических изображений для каждой информационной карточки
- Поддержка операций CRUD для информационных карточек
- Обработка ошибок при работе с сервером
- Графические изображения могут быть ф формате JPG, PNG. Достаточно поддержки одного из форматов
- Сортировка информационных карточек по названию
- Возможность удаления нескольких информационных карточек за одну операцию
- Проверка ошибок ввода информационных карточке – пустое название и отсутствующее изображение
- .Net 6.0
- JSON Serialization & Deserialization
- AutoMapper (Server)
- SOLID, HTTP RESTfull
- Swagger UI (Server)
- WPF (Client)
- xUnit.net
Разработка проекта велась в течение 1 недели, в рамках которой было разработано клиент-серверное приложение на базе WPF
+ .Net Core
, которое представляет из себя сервер, управляющий доступом к файловому хранилищу по отношению к одному, либо же сразу нескольким клиентам.
Клиентская часть была написана на базе
WPF
с использованием асинхронных методов и принципов многопоточного программирования (async
-await
). Пользователь имеет возможность добавлять новые информационные карточки, получать их с сервера, редактировать и удалять уже существующие карточки (CRUD Functionality
).
GUI представляет из себя меню с левой стороны экрана, которое отражает в себе весь функционал приложения, а также основной экран, который меняется в зависимости от только что нажатой пользователем кнопки левого меню.
Dashboard
- Активирует главную панель, где отображаются все доступные информационные карточкиAdd New Card
- Активирует панель, на которой пользователь сможет добавлять новые карточкиUpdate Card
- Активирует панель, на которой пользователь сможет изменять выбранную им на главной панели карточкуDelete Card
- Удаляет выбранную (выбранные) пользователем на главной панели карточку (карточки)Connection
- Активирует панель, которая показывает текущее состояние соединения с серверомLogout
- При нажатии, происходит выход из приложения
На главной странице расположена таблица, в которой отображаются все доступные информационные карточки
Зелёная кнопка
- Выбрать элемент; Если элемент уже выбран (О чём свидетельствует галочка напротив), отменить выборКрасная кнопка
- Выбрать элемент и сразу же удалить егоЖёлтая кнопка
- Выбрать элемент и перейти на страницу с его редактированием
Также, вверху есть 2 кнопки, отвечающие за сортировку:
Unsorted
- Привоит все карточки к их первоначальному порядку храненияSorted By Name
- Сортирует все приведенные на главное панели карточки по алфавиту
Наряду с кнопками сортировки, слева приведен
Counter
, который отображает количество имеющихся карточек
При добавлении новой карточки, активируется страница, на которой пользователю необходимо будет ввести название, а также установить картинку для информационной карточки. После чего, если данные были введены верно, нажать кнопку
Save Card
. Таким образом, пользователь сохранит карточку на сервере и будет перенаправлен на главный экран.
Если же при добавлении карточки в одном из полей была допущена ошибка, появится соответствующее сообщение, которое предложит пользователю отредактировать введённые им данные.
При выборе карточки на главной панели, активируется страница, на которой пользователю необходимо будет произвести изменения в текущей карточке (либо же не производить их, если он, вдруг, передумает). Если данные были изменены верно, нажать кнопку
Save Card
. Таким образом, пользователь обновит карточку на сервере и будет перенаправлен на главный экран.
Если же при изменении карточки была допущена ошибка, появится соответствующее сообщение, которое предложит пользователю отредактировать введённые им данные.
Если пользователь выбрал хотя бы одну карточку на главном экране, будет произведено удаление всех выбранных им карточек, после чего он будет перенаправлен на главную страницу.
Если пользователь не выбрал ни одну из карточек, появится сообщение, которое предложит ему выбрать хотя бы одну карточку для удаления.
Перед тем, как начать пользоваться приложением, пользователю необходимо будет установить соединение с сервером. О том, установлено ли соединение с сервером, будет свидетельствовать соответствующая надпись на странице.
Чёрная
- Ожидается запрос на подключение к серверуКрасная
- Не удалось установить соединение с серверомЗелёная
- Соединение с сервером установлено
Если соединение было установлено успешно, пользователю будет открыт доступ к функционалу приложения.
Если при работе с сервером у клиента произойдёт сбой, он будет перенаправлен на эту страницу с информацией об ошибке, после чего ему повторно необходимо будет устанавливать соединение для дальнейшей работы.
Завершение работы программы
Таким образом, удалось создать полноценное GUI
, которое включает в себя все самые основные операции CRUD
, позволяющее пользователю не думать о том, как устроено взаимодействие изнутри и спокойно пользоваться приложением.
На стороне сервера были реализованы такие
Endpoints
, как:
URI | Метод | Операция | Описание | Успешное выполнение | Ошибка |
---|---|---|---|---|---|
api/InformationCards | GET | READ | Получение всех карточек | 200 ОК | 404 Bad Request / 404 Not Found |
api/InformationCards/{id} | GET | READ | Получение карточки по её id | 200 ОК | 404 Bad Request / 404 Not Found |
api/InformationCards | POST | CREATE | Добавление в файл новой карточки | 201 Created | 404 Bad Request / 404 Not Found |
api/InformationCards/{id} | PUT | UPDATE | Обновление всей карточки (Без затрагивания Id) | 204 No Content | 404 Bad Request / 404 Not Found |
api/InformationCards/{id} | DELETE | DELETE | Удаление карточки по её id | 200 ОК \ 204 No Content | 404 Bad Request / 404 Not Found |
Все приведенные выше
Endpoints
были протестированы при помощиUnit
-тестов и библиотекиxUnit.net
.
Изначально, мной было принято решение – хранить всё на стороне сервера: и название, и саму картинку в виде
набора байт
. Картинка поступала со стороны клиента, обрабатывалась, переводилась впоследовательность байт
, после чего эта последовательность конвертировалась в строку форматаbase64
, записывалась вJSON
-строку, и после чего, итоговая строка отправлялась на сервер, который в свою очередь сохранял её в файл форматаJSON
(решение использовать такие сложные манипуляции было принято по причине, что формат данныхJSON
не поддерживает хранение в нёммассива байтов
как целостной структуры), после чего отправлял обратно результат операции в видестроки
, которая на стороне клиента обрабатывалась, и все необходимые данные по картинке сначала декодировались из форматаbase64
вмассив байт
, после чего из этого массива собиралась картинка и отправлялась на представление пользователю. ОДНАКО, при тестировании на 3-х и более картинках, ожидание ответа сервера уже занимало порядка 30 секунд, так как приходилось работать с большим объёмом данных в обе стороны. Поэтому, мной было принято решение хранить картинки локально, а ссылки на них – на стороне сервера в видестроки
. Я понимаю, что в реальности, это не имеет никакого смысла, так как каждый пользователь будет получать доступ только к своим картинкам (которые у него будут храниться локально на клиентской части), в то время как сервер будет хранить информацию о карточках абсолютно всех клиентов, зато это позволило в несколько десятков раз увеличить скорость работы приложения, что положительно скажется на итоговом представлении программы пользователями.
Так как мне пришлось работать с многопоточностью, чтоб не блокировать пользовательский интерфейс, при тестировании возникала такая ситуация, что картинку нельзя было удалить на стороне клиента, так как она в это время использовалась в другом потоке (не вызывающем метод на удаление). Мной было принято решение – удалять все неиспользуемые картинки сразу после того, как пользователь выключает приложение. Если задуматься, такой подход никак не повлияет на функционал программы, так как картинки будут отображаться согласно пришедшей с сервера информации. Так как на сервере у модели будет указан уже другой путь, то и эта картинка нигде не будет использоваться (разве что в каком-то из незавершённых потоков), следовательно, рано или поздно, картинка всё же будет удалена. Даже если по какой-то причине картинка не будет удалена и при выключении приложения (такая ситуация тоже может возникнуть), то при следующем запуске приложения, какой-то работы в нём и последующего выключения приложения, эти неиспользуемые картинки будут удалены с сервера со 100-процентной вероятностью, так как содержащих эти картинки потоков просто не будет существовать.
Как уже было сказано выше, сервер хранит информацию сразу от нескольких клиентов, при этом на запрос
GET
клиента, сервер отсылает ему сразу все модели, содержащиеся на его стороне. Так как, собственно, проходит отсеивание ненужной информации, информации о чужих данных? На самом деле всё очень просто – мы получаем список всех моделей со стороны сервера, получаем список всех наших картинок, хранящихся на стороне клиента, после чего проверяем картинку каждой из пришедшей к нам моделей на вхождение в список картинок клиента, и, если совпадений не было найдено – то эта модель принадлежит другому клиенту, следовательно, к себе в список мы её не сохраняем и перебираем список моделей с сервера дальше, в поисках таких моделей, которые будут иметь ссылку на картинку, хранящуюся у нас. Таким незамысловатым способом происходит отсеивание ненужной информации.
По поводу сохранения модели. Почему после метода
POST
я использовал методGET
для всех моделей, а неGET
для единичной, только что создавшейся? Это я решил сделать из соображений, как должен работать реальный сервер, на котором хранят свои данные сразу несколько клиентов, и таким образом обмениваются информацией. Да, соглашусь, в моём случае (где картинки хранятся локально) это не имеет большого смысла, и для лучшей скорости работы, лучше было бы использовать методGET
для единичной модели. ОДНАКО, возвращаясь к моему первоначальному видению, где все картинки хранились бы как набор байтов в строке, после того, как мы отправили картинку на сервер, в это время кто-то другой, такой же пользователь, мог бы отправить свою собственную картинку с именем, таким образом, на сервере появились бы сразу 2 новые картинки. Конечно, можно было бы вести лог всех запросов, проверять методы, смотреть что куда приходит и куда что нужно отправлять, однако это бы увеличило нагрузку самой бизнес модели, и вероятность ошибки при таком подходе намного больше, нежели в моём, где одним методомGET
мы получаем сразу все состояния моделей на стороне сервера. Таким образом, если бы модели хранились как набор байт, то получение сразу всех моделей позволило бы сразу обоим пользователям увидеть добавленные друг другом картинки на сервер, что, конечно же, максимально приближено к работе реального сервера.
Таким образом, был создан полностью фонкционирующий сервер, способный работать сразу с несколькими клиентами и не быть привязанным к какому-то одному.