Введение
В предыдущей статье TeamWox SDK: Построение пользовательского интерфейса вы ознакомились с концепцией создания пользовательского интерфейса в системе TeamWox на примере нескольких типовых элементов управления. При этом отображаемые данные были статичными, т.е. мы не могли их изменять.
В этой статье мы рассмотрим организацию постоянного хранения и изменения данных. Для этой цели в TeamWox применяется embedded версия СУБД Firebird. Продолжая работать с учебным модулем Hello World, в его менеджер мы добавим новые методы работы с данными, так чтобы они загружались не из статических массивов, а из базы данных.
В рамках процесса развертывания модуля, мы спроектируем простую таблицу в БД, а затем организуем запросы на вывод и изменение ее значений.
Затем, вооружившись навыками, полученными из предыдущей статьи, мы доработаем существующий пользовательский интерфейс так, чтобы с его помощью можно было создавать, отображать, изменять и удалять данные из таблицы.
Более правильным будет организовать кеширование данных в памяти. О том, как реализовать кеширование и расширить архитектуру менеджера, мы расскажем в одной из следующих статей.
Развертывание модулей TeamWox
Развертывание - это один из этапов жизненного цикла модуля TeamWox. Условно жизненный цикл модуля можно представить так:
- Этап 1. Разработка модуля
- Этап 2. Установка и настройка модуля на сервере TeamWox
- Этап 3. Выявление ошибок, сбор замечаний и предложений от пользователей
- Этап 4. Расширение функционала модуля, исправление ошибок
- Этап 5. Обновление модуля на сервере
Затем этапы 3, 4 и 5 циклически повторяются в течение всего срока поддержки модуля его разработчиками.
Мы расширим функциональность модуля Hello World, добавив в него возможность работы с данными, которые будут храниться в СУБД. Но прежде всего, нам потребуется создать таблицу с определенной структурой (при необходимости, можно также проинициализировать таблицу данными). При каждом последующем обновлении модуля структура таблицы будет проверяться и при необходимости изменяться.
SDK предоставляет все необходимые средства для работы с СУБД TeamWox.
1. В проекте HelloWorld.vcproj
в stdafx.h
включите файл smart_sql.h
. Этот инструмент представляет собой набор классов для удобной работы с БД средствами SQL (расположен в папке \TeamWox SDK\SDK\Common
). Соблюдайте очередность подключаемых файлов. Также добавьте его в проект (папка \Header Files\Common
).
#include "..\..\SDK\Common\SmartLogger.h" #include "..\..\SDK\Common\smart_sql.h" #include "..\..\SDK\Common\tools_errors.h" #include "..\..\SDK\Common\tools_strings.h" #include "..\..\SDK\Common\Page.h"
2. Опишите структуру таблицы. Для этого в менеджере создайте функцию с именем DBTableCheck
.
//+------------------------------------------------------------------+ //| Менеджер модуля | //+------------------------------------------------------------------+ class CHelloWorldManager { private: static HelloWorldRecord m_info_records[]; // список общедоступной информации static HelloWorldRecord m_advanced_records[]; // список информации с ограниченным доступом //--- IServer *m_server; // ссылка на сервер //--- CSync m_sync; // синхронизация доступа к членам класса public: CHelloWorldManager(); ~CHelloWorldManager(); //--- TWRESULT Initialize(IServer *server); //--- работа с информацией TWRESULT InfoGet(const Context *context,HelloWorldRecord *records,int *count); TWRESULT InfoAdvacedGet(const Context *context,HelloWorldRecord *records,int *count); private: TWRESULT DBTableCheck(ISqlBase *sql); // проверка/создание/изменение структуры таблицы };
//+------------------------------------------------------------------+ //| Создание таблицы HELLOWORLD | //+------------------------------------------------------------------+ TWRESULT CHelloWorldManager::DBTableCheck(ISqlBase *sql) { //--- проверки if(sql==NULL) ReturnError(RES_E_INVALID_ARGS); //--- TWRESULT res=RES_S_OK; //--- if(RES_FAILED(res=sql->CheckTable("HELLOWORLD", "ID BIGINT DEFAULT 0 NOT NULL," "NAME VARCHAR(256) DEFAULT '' NOT NULL", "PRIMARY KEY (ID)", "DESCENDING INDEX IDX_HELLOWORLD_ID_DESC (ID)", NULL,NULL,0))) ReturnError(res); //--- ExtLogger(NULL,LOG_STATUS_INFO) << "Table 'HELLOWORLD' checked"; //--- return(RES_S_OK); }
ISqlBase::CheckTable
проверяет/создает/изменяет таблицу и ее структуру. Описания полей таблицы разделяются запятыми и экранируются кавычками, поскольку вся эта совокупность передается как второй аргумент функции CheckTable
.Текст, который описывает поля в CheckTable, соответствует синтаксису
CREATE TABLE [имя таблицы] ([описание полей]) [параметры таблицы]
.Ключевые этапы работы модуля всегда рекомендуется записывать в лог, поскольку это существенно облегчает поиск ошибок при работе модуля на сервере у клиента. Для этой цели в SDK имеется специальный класс
CSmartLogger
.В нашем случае, сообщение о работе метода
CheckTable
мы выводим в лог с помощью объекта ExtLogger
этого класса. Сообщения в лог лучше выводить на английском языке из-за возможного конфликта кодировок.3. Создайте подключение менеджера к БД сервера. Для этого в \Managers\HelloWorldManager.cpp
добавьте следующие строчки кода.
//+------------------------------------------------------------------+ //| Инициализируем модуль | //+------------------------------------------------------------------+ TWRESULT CHelloWorldManager::Initialize(IServer *server) { if(server==NULL) ReturnError(RES_E_INVALID_ARGS); //--- TWRESULT res=RES_S_OK; //--- CLocker lock(m_sync); //--- m_server=server; //--- CSmartSql sql(server); if(sql==NULL) ReturnErrorExt(RES_E_SQL_ERROR,NULL,"failed to make sql connection"); //--- if(RES_FAILED(res=DBTableCheck(sql))) ReturnErrorExt(res,NULL,"failed to check table"); //--- return(RES_S_OK); }
4. В структуре HelloWorldRecord
(файл \API\HelloWorld.h
) приведите в соответствие типы данных для использования в SQL-запросе (идентификаторы записей в TeamWox рекомендуется делать 64 разрядными).
//+------------------------------------------------------------------+ //| Структура записи | //+------------------------------------------------------------------+ struct HelloWorldRecord { INT64 id; wchar_t name[256]; };
Скомпилируйте модуль и запустите сервер TeamWox. В окне консоли выведется сообщение о том, что таблица HELLOWORLD
проверена (создана).
HELLOWORLD
с полями ID
и NAME
. Все реализуется в исходном коде менеджера, и никаких дополнительных инструментов для этого не нужно.
Методы менеджера для работы с данными
Работа с любой СУБД базируется на четырех фундаментальных операциях CRUD:
- Создание новых записей (Create)
- Чтение записей (Retrieve)
- Редактирование записей (Update)
- Удаление записей (Delete)
Эти методы обеспечивают полноценную работу с данными. Их мы реализуем в менеджере модуля, а в странице мы создадим HTTP API, с помощью которого можно будет запрашивать данные в формате JSON.
В менеджере объявите основные, а также вспомогательные методы и члены.
//+------------------------------------------------------------------+ //| Менеджер модуля | //+------------------------------------------------------------------+ class CHelloWorldManager { private: static HelloWorldRecord m_info_records[]; // список общедоступной информации static HelloWorldRecordAdv m_advanced_records[]; // список информации с ограниченным доступом //--- IServer *m_server; // ссылка на сервер //--- CSync m_sync; // синхронизация доступа к членам класса //--- INT64 m_next_id; // Следующий id записи public: CHelloWorldManager(); ~CHelloWorldManager(); //--- TWRESULT Initialize(IServer *server, int prev_build); //--- работа с информацией TWRESULT InfoAdvacedGet(const Context *context,HelloWorldRecordAdv *records,int *count); //--- Методы C.R.U.D. (Create, Retrieve, Update и Delete) //--- ЧТЕНИЕ (Retrieve) // Получение списка публичной информации TWRESULT InfoGet(const Context *context,HelloWorldRecord *records,int start,int *count); // Получение строки из таблицы HELLOWORLD TWRESULT InfoGet(const Context *context,INT64 id,HelloWorldRecord *record); // Получение количества записей в таблице HELLOWORLD TWRESULT InfoCount(const Context *context,int *count); //--- СОЗДАНИЕ (Create) и РЕДАКТИРОВАНИЕ (Update) //--- Изменение записи или добавление новой записи в таблице HELLOWORLD TWRESULT InfoUpdate(const Context *context,HelloWorldRecord *record); //--- УДАЛЕНИЕ (Delete) //--- Удаление записи из таблицы HELLOWORLD TWRESULT InfoDelete(const Context *context,INT64 id); private: TWRESULT DBTableCheck(ISqlBase *sql); // проверка/создание/изменение структуры таблицы //--- Определение максимального id записи из таблицы HELLOWORLD TWRESULT LoadMaxId(ISqlBase *sql); //--- Получение следующего идентификатора INT64 NextId(); };
1. Чтение записей (Retrieve)
Начнем с чтения, которое включает в себя поиск, извлечение и просмотр записей из таблицы.
1.1. Для вывода данных из базы нам потребуется в менеджере модуля изменить существующую функцию InfoGet
, которая получает данные для отображения на страницах.
//+------------------------------------------------------------------+ //| Получение списка публичной информации | //+------------------------------------------------------------------+ TWRESULT CHelloWorldManager::InfoGet(const Context *context,HelloWorldRecord *records,int start,int *count) { HelloWorldRecord rec_info_get ={0}; //--- проверки (параметр start должен начинаться с 1, // поскольку так же с 1 начинаются записи в таблицах СУБД Firebird) if(context==NULL || m_server==NULL || records==NULL || start<1 || count==NULL || *count<=0) ReturnError(RES_E_INVALID_ARGS); if(context->sql==NULL) ReturnError(RES_E_INVALID_CONTEXT); //--- Инициализация переменных для цикла получения результатов запроса int max_count =*count; int index =0; int end =start+max_count; // номер последней записи *count =0; //--- текст SQL-запроса на выборку записей из таблицы HELLOWORLD с сортировкой по полю ID. char query_select[] ="SELECT id,name FROM helloworld ORDER BY id ROWS ? TO ?"; //--- "Привязываем" данные к параметрам запроса SqlParam params_query_select[] ={ SQL_INT64,&rec_info_get.id, sizeof(rec_info_get.id), SQL_WTEXT, rec_info_get.name,sizeof(rec_info_get.name) }; //--- Параметры запроса SqlParam params_rows[] ={ SQL_LONG,&start,sizeof(start), SQL_LONG,&end, sizeof(end) }; //--- шлем запрос if(!context->sql->Query(query_select, params_rows, _countof(params_rows), params_query_select, _countof(params_query_select))) ReturnErrorExt(RES_E_SQL_ERROR,NULL,"HELLOWORLD records query failed"); //--- CLocker lock(m_sync); //--- в цикле получим все элементы результатов запроса while(context->sql->QueryFetch()) { if(*count<max_count) { memcpy(&records[*count],&rec_info_get,sizeof(records[*count])); //--- (*count)++; } //--- index++; } //--- context->sql->QueryFree(); //--- return(RES_S_OK); }
ISqlBase::Query
выполняет запрос (с указанными параметрами) на чтение данных из таблицы HELLOWORLD
без их изменения. В тексте SQL-запроса SELECT
с помощью операнда ROWS
ограничивается диапазон извлекаемых из таблицы строк.Метод
ISqlBase::QueryFetch
по одной извлекает строки из выборки после запроса на чтение. Эти строки записываются в массив records
и, таким образом, подготавливаются для последующего отображения на странице.С помощью метода
ISqlBase::QueryFree
запрос и его результаты освобождаются.1.2. Функция InfoCount
будет использоваться для получения количества записей из БД.
//+------------------------------------------------------------------+ //| Получение количества записей в таблице HELLOWORLD | //+------------------------------------------------------------------+ TWRESULT CHelloWorldManager::InfoCount(const Context *context,int *count) { HelloWorldRecord rec_page_count ={0}; //--- проверки if(context==NULL || m_server==NULL || count==NULL) ReturnError(RES_E_INVALID_ARGS); if(context->sql==NULL) ReturnError(RES_E_INVALID_CONTEXT); //--- текст SQL-запроса количества записей ID в таблице HELLOWORLD char query_page_count[] ="SELECT COUNT(id) FROM helloworld"; //---"Привязываем" данные к параметрам запроса SqlParam params_query_page_count[] ={ SQL_LONG,count,sizeof(*count) }; //--- шлем запрос if(!context->sql->Query(query_page_count, NULL,0, params_query_page_count,_countof(params_query_page_count))) ReturnErrorExt(RES_E_SQL_ERROR,NULL,"failed to query helloworld records"); //--- обеспечим атомарность CLocker lock(m_sync); //--- в цикле получим все элементы if(!context->sql->QueryFetch()) ReturnErrorExt(RES_E_SQL_ERROR,NULL,"failed to get count"); //--- context->sql->QueryFree(); //--- return(RES_S_OK); }
Context
) доступно уже созданное соединение, и дополнительно создавать его не надо. В сервере есть пул соединения с СУБД.Соединение с базой данных создается только в случае вызова методов для запроса. Иными словами, если вы просто запросили SQL соединение, но не воспользовались им, то реальное соединение с базой данных не будет запрошено.
2. Создание новых (Create) и редактирование существующих (Update) записей
2.1. Проинициализируйте переменную m_next_id
:
//+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ CHelloWorldManager::CHelloWorldManager():m_server(NULL),m_next_id(0) { //--- //--- }
2.2. Функция LoadMaxId
с помощью SQL-запроса извлекает максимальный номер id (т.е. номер последней записи) из таблицы HELLOWORLD
.
//+------------------------------------------------------------------+ //| Получение максимального id записи из таблицы HELLOWORLD | //+------------------------------------------------------------------+ TWRESULT CHelloWorldManager::LoadMaxId(ISqlBase *sql) { //--- проверки if(sql==NULL) ReturnError(RES_E_INVALID_ARGS); //--- текст SQL-запроса на получение максимального значения поля ID char query_select_max[] ="SELECT MAX(id) FROM helloworld"; //--- Параметр запроса SqlParam params_max_id[] ={ SQL_INT64,&m_next_id,sizeof(m_next_id) }; //--- шлем запрос if(!sql->Query(query_select_max,NULL,0,params_max_id,_countof(params_max_id))) ReturnErrorExt(RES_E_SQL_ERROR,NULL,"failed query to get max id from helloworld"); //--- получим значение sql->QueryFetch(); sql->QueryFree(); //--- return(RES_S_OK); }
2.3. Функция NextId
увеличивает значение переменной m_next_id
на единицу. Она будет использоваться при добавлении новых записей.
//+------------------------------------------------------------------+ //| Получение следующего id записи | //+------------------------------------------------------------------+ INT64 CHelloWorldManager::NextId() { //--- CLocker lock(m_sync); return(++m_next_id); //--- }
m_next_id
. TeamWox - это сервер, который одновременно обрабатывает множество запросов.2.4. Добавьте проверку на результат выполнения этой функции в инициализацию менеджера.
//+------------------------------------------------------------------+ //| Инициализируем модуль | //+------------------------------------------------------------------+ TWRESULT CHelloWorldManager::Initialize(IServer *server, int prev_build) { if(server==NULL) ReturnError(RES_E_INVALID_ARGS); //--- TWRESULT res=RES_S_OK; //--- CLocker lock(m_sync); //--- m_server=server; //--- CSmartSql sql(server); if(sql==NULL) ReturnErrorExt(RES_E_SQL_ERROR,NULL,"failed to make sql connection"); //--- if(RES_FAILED(res=DBTableCheck(sql))) ReturnErrorExt(res,NULL,"failed to check table"); //--- проверка максимального ID для записи в таблице HELLOWORLD if(RES_FAILED(res=LoadMaxId(sql))) ReturnErrorExt(res,NULL,"failed to load max id for HELLOWORLD table"); //--- if(prev_build<101) { //--- При необходимости внесем изменения в данные при обновлении старой версии модуля } //--- return(RES_S_OK); }
2.5. Метод InfoUpdate
будет добавлять новые записи в таблицу HELLOWORLD
, либо изменять существующие.
//+------------------------------------------------------------------+ //| Добавление, сохранение записи в таблицу HELLOWORLD | //+------------------------------------------------------------------+ TWRESULT CHelloWorldManager::InfoUpdate(const Context *context,HelloWorldRecord *record) { //--- проверки if(context==NULL || record==NULL) ReturnError(RES_E_INVALID_ARGS); if(context->sql==NULL) ReturnError(RES_E_INVALID_CONTEXT); //--- текст SQL-запроса на добавление новой записи в таблицу HELLOWORLD char query_insert[] ="INSERT INTO helloworld(name,id) VALUES(?,?)"; //--- текст SQL-запроса на изменение существующей записи в таблице HELLOWORLD char query_update[] ="UPDATE helloworld SET name=? WHERE id=?"; //--- "Привязываем" данные к параметрам запроса SqlParam params_query[] ={ SQL_WTEXT, record->name, sizeof(record->name), SQL_INT64,&record->id, sizeof(record->id) }; //--- новая запись? if(record->id<=0) { record->id=NextId(); //--- if(!context->sql->QueryImmediate(query_insert, params_query, _countof(params_query))) ReturnErrorExt(RES_E_SQL_ERROR,NULL,"failed to insert helloworld record"); } else { if(!context->sql->QueryImmediate(query_update, params_query, _countof(params_query))) ReturnErrorExt(RES_E_SQL_ERROR,NULL,"failed to update helloworld record"); } //--- return(RES_S_OK); }
ISqlBase::QueryImmediate
используется в случаях, когда не требуется получать результаты запроса (в отличие от ISqlBase::Query
).2.6. Во второй версии метода InfoGet
выполняется SQL-запрос на получение строки из таблицы HELLOWORLD
по указанному ID
.
//+------------------------------------------------------------------+ //| Получение строки из таблицы HELLOWORLD | //+------------------------------------------------------------------+ TWRESULT CHelloWorldManager::InfoGet(const Context *context,INT64 id,HelloWorldRecord *record) { //--- проверки if(context==NULL || record==NULL || id<1) ReturnError(RES_E_INVALID_ARGS); if(context->sql==NULL) ReturnError(RES_E_INVALID_CONTEXT); //--- текст SQL-запроса на получение строки из таблицы HELLOWORLD по указанному ID. char query_select_string[]="SELECT id,name FROM helloworld WHERE id=?"; //--- Параметр запроса SqlParam params_string[]={ SQL_INT64,&id,sizeof(id) }; //--- "Привязываем" данные к параметрам запроса SqlParam params_query_select_string[] ={ SQL_INT64,&record->id, sizeof(record->id), SQL_WTEXT, record->name, sizeof(record->name) }; //--- ZeroMemory(record,sizeof(*record)); //--- шлем запрос if(!context->sql->Query(query_select_string, params_string, _countof(params_string), params_query_select_string, _countof(params_query_select_string))) ReturnErrorExt(RES_E_SQL_ERROR,NULL,"helloworld record query failed"); //--- получим элемент context->sql->QueryFetch(); context->sql->QueryFree(); //--- if(record->id!=id) return(RES_E_NOT_FOUND); //--- return(RES_S_OK); }
3. Удаление записей (Delete)
Метод InfoDelete
реализуется проще всего.
//+------------------------------------------------------------------+ //| Удаление записи из таблицы HELLOWORLD | //+------------------------------------------------------------------+ TWRESULT CHelloWorldManager::InfoDelete(const Context *context,INT64 id) { //--- проверки if(context==NULL || id<=0) ReturnError(RES_E_INVALID_ARGS); if(context->sql==NULL) ReturnError(RES_E_INVALID_CONTEXT); //--- текст SQL-запроса на удаление строки из таблицы HELLOWORLD по указанному ID char delete_string[]="DELETE FROM helloworld WHERE id=?"; //--- Параметр запроса SqlParam params_delete_string[] ={ SQL_INT64,&id,sizeof(id) }; //--- if(!context->sql->QueryImmediate(delete_string,params_delete_string,_countof(params_delete_string))) ReturnErrorExt(RES_E_SQL_ERROR,NULL,"failed to delete helloworld record"); //--- ExtLogger(context,LOG_STATUS_INFO) << "Delete record #" << id; //--- return(RES_S_OK); }
HTTP API для работы с данными
Теперь нам нужно создать HTTP API, который будет использовать реализованные методы C.R.U.D. Отображение и удаление записей будет выполняться на уже существующей странице PageNumberTwo, а для создания и редактирования записей мы отведем новую страницу PageEdit. Данные, полученные страницей из менеджера, будут выводиться как объекты JSON.
Вывод и удаление данных
1. В странице PageNumberTwo объявите функции, которые будут использовать методы чтения и удаления записей, а также вспомогательные члены.
//+------------------------------------------------------------------+ //| Вторая страница | //+------------------------------------------------------------------+ class CPageNumberTwo : public CPage { private: IServer *m_server; //--- HelloWorldRecord m_info[5]; // количество выводимых на странице записей HelloWorldRecord *m_info_current; CHelloWorldManager *m_manager; // подключение к менеджеру int m_info_count; //--- для нумератора таблицы HELLOWORLD int m_start; // номер текущей записи int m_info_total; // общее количество записей //--- HelloWorldRecordAdv m_info_advanced[5]; HelloWorldRecordAdv *m_info_advanced_current; int m_info_advanced_count; public: CPageNumberTwo(); ~CPageNumberTwo(); //--- обработчик страницы TWRESULT Process(const Context *context, IServer *server, const wchar_t *path, CHelloWorldManager *manager); //--- функция показа bool Tag(const Context *context,const TagInfo *tag); //--- ЧТЕНИЕ (Read) //--- Вывод данных на страницу в формате JSON TWRESULT JSONDataOutput(const Context *context,CHelloWorldManager *manager); //--- УДАЛЕНИЕ (Delete) //--- Удаление строки TWRESULT OnDelete(const Context *context, IServer *server, const wchar_t *path, private: //--- Подготовка списка страниц нумератора TWRESULT PreparePages(const Context *context,CHelloWorldManager *manager); };
2. Проинициализируйте вспомогательные члены в конструкторе страницы. Эти переменные будут использоваться для создания специального элемента управления — нумератора страниц — который отображает на странице заданное количество записей, а остальные располагает на следующих вкладках.
//+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ CPageNumberTwo::CPageNumberTwo() : m_server(NULL),m_start(0),m_info_total(0), m_info_count(0),m_info_advanced_count(0), m_info_advanced_current(NULL),m_info_current(NULL) { //--- ZeroMemory(m_info, sizeof(m_info)); ZeroMemory(m_info_advanced,sizeof(m_info_advanced)); //--- }
3. Метод подготовки страниц нумератора будет вычислять максимальное число записей, а также номер записи, с которой начинать отображать данные.
//+------------------------------------------------------------------+ //| Подготовка списка страниц нумератора | //+------------------------------------------------------------------+ TWRESULT CPageNumberTwo::PreparePages(const Context *context,CHelloWorldManager *manager) { TWRESULT res = RES_S_OK; //--- if(context==NULL || manager==NULL) ReturnError(RES_E_INVALID_ARGS); if(context->request==NULL) ReturnError(RES_E_INVALID_CONTEXT); //--- if(RES_FAILED(res=manager->InfoCount(context,&m_info_total))) ReturnErrorExt(res,context,"failed get count of records"); //--- m_start=max(1,context->request->GetInt32(IRequest::GET,L"from")+1); int max_page =max(0,int((m_info_total+_countof(m_info))/_countof(m_info))-1); //--- if(m_start>max_page*_countof(m_info)+1) m_start=max_page*_countof(m_info)+1; return(res); }
4. Метод вывода записей в виде массива в формате JSON.
//+------------------------------------------------------------------+ //| Вывод данных на страницу в формате JSON | //+------------------------------------------------------------------+ TWRESULT CPageNumberTwo::JSONDataOutput(const Context *context,CHelloWorldManager *manager) { TWRESULT res = RES_S_OK; //--- if(context==NULL || manager==NULL) ReturnError(RES_E_INVALID_ARGS); if(context->response==NULL) ReturnError(RES_E_INVALID_ARGS); //--- m_info_count=_countof(m_info); if(RES_FAILED(res=manager->InfoGet(context,m_info,m_start,&m_info_count))) ReturnErrorExt(res,context,"failed get info records"); //--- CJSON json(context->response); json << CJSON::ARRAY; for(int i=0;i<m_info_count;i++) { json << CJSON::OBJECT; //--- json << L"id" << m_info[i].id; json << L"name" << m_info[i].name; //--- json << CJSON::CLOSE; } //--- json << CJSON::CLOSE; return(RES_S_OK); }
5. В CPageNumberTwo::Tag
поместите вызов этого метода в токен info_list
.
TWRESULT CPageNumberTwo::Tag(const Context *context,const TagInfo *tag) { .............................. //--- if(TagCompare(L"info_list",tag)) { JSONDataOutput(context,m_manager); //--- return(false); } .............................. }
6. Измените блок обработки запроса, добавив туда вызов JSONDataOutput
и PreparePages
.
//+------------------------------------------------------------------+ //| Обработка запроса | //+------------------------------------------------------------------+ TWRESULT CPageNumberTwo::Process(const Context *context, IServer *server, const wchar_t *path, CHelloWorldManager *manager) { //--- проверки if(context==NULL || path==NULL || manager==NULL || server==NULL) ReturnError(RES_E_INVALID_ARGS); if(context->request==NULL || context->user==NULL) ReturnError(RES_E_INVALID_CONTEXT); //--- TWRESULT res=RES_S_OK; //--- m_server=server; m_manager = manager; //--- Подготовка списка страниц нумератора if(RES_FAILED(res=PreparePages(context,manager))) return(res); //--- if(context->user->PermissionCheck(HELLOWORLD_MODULE_ID,HELLOWORLD_PERM_ADVANCED)) { m_info_advanced_count=_countof(m_info_advanced); if(RES_FAILED(res=manager->InfoAdvacedGet(context,m_info_advanced,&m_info_advanced_count))) ReturnErrorExt(res,context,"failed get advanced info records"); } //--- AJAX-запрос данных для вывода на страницу if(context->request->AJAXRequest()) { return(JSONDataOutput(context,manager)); } //--- отображаем страницу return(server->PageProcess(context, L"templates\\number2.tpl", this, TW_PAGEPROCESS_NOCACHE)); }
7. Реализуйте метод удаления записи. Он проверяет, существует ли запись в таблице по указанному id, удаляет ее методом InfoDelete
, а затем генерирует ответ в формате JSON, который просто перестраивает старые данные с учетом выполненных изменений.
//+------------------------------------------------------------------+ //| Удаление записи | //+------------------------------------------------------------------+ TWRESULT CPageNumberTwo::OnDelete(const Context *context, IServer *server, const wchar_t *path, CHelloWorldManager *manager) { INT64 id =0; TWRESULT res=RES_S_OK; //--- проверки if(context==NULL || path==NULL || manager==NULL) ReturnError(RES_E_INVALID_ARGS); if(context->request==NULL || context->response==NULL) ReturnError(RES_E_INVALID_CONTEXT); //--- if(!context->request->Exist(IRequest::POST,L"id") || (id=context->request->GetInt64(IRequest::POST,L"id"))<=0) return(RES_E_NOT_FOUND); //--- if(RES_FAILED(res=manager->InfoDelete(context,id))) ReturnErrorExt(res,context,"failed to delete record"); //--- if(RES_FAILED(res=PreparePages(context,manager))) return(res); //--- CJSON json(context->response); json << CJSON::OBJECT << L"from" << m_start << L"count" << m_info_total << L"data" << CJSON::DATA; if(RES_FAILED(res = JSONDataOutput(context,manager))) return(res); json << CJSON::CLOSE << CJSON::CLOSE; return(res); }
Добавление и изменение данных
1. Создайте в проекте новую страницу PageEdit
, взяв за основу исходный код существующих страниц. Объявите в ней функцию, которая будет использовать методы создания новых и изменения существующих записей.
//+------------------------------------------------------------------+ //| Страница редактирования | //+------------------------------------------------------------------+ class CPageEdit : public CPage { private: IServer *m_server; HelloWorldRecord m_record; public: CPageEdit(); ~CPageEdit(); //--- обработчик TWRESULT Process(const Context *context, IServer *server, const wchar_t *path, CHelloWorldManager *manager); //--- функции показа bool Tag(const Context *context,const TagInfo *tag); //--- СОЗДАНИЕ (Create) и РЕДАКТИРОВАНИЕ (Update) TWRESULT OnUpdate(const Context *context, IServer *server, const wchar_t *path, CHelloWorldManager *manager); };
2. При запросе данных методом InfoGet
получите строку из таблицы и вызовите для нее метод OnUpdate
.
//+------------------------------------------------------------------+ //| Обработка запроса | //+------------------------------------------------------------------+ TWRESULT CPageEdit::Process(const Context *context, IServer *server, const wchar_t *path, CHelloWorldManager *manager) { TWRESULT res=RES_S_OK; INT64 id =0; //--- проверки if(context==NULL || path==NULL || server==NULL || manager==NULL) ReturnError(RES_E_INVALID_ARGS); if(context->request==NULL) ReturnError(RES_E_INVALID_CONTEXT); //--- m_server=server; //--- запросим данные if(PathCompare(L"number_two/edit/",path)) { id=_wtoi64(path+16); // 16 - длина пути 'number_two/edit/' //--- if(id>0 && RES_FAILED(res=manager->InfoGet(context,id,&m_record))) ReturnErrorExt(res,context,"failed to get record"); } //--- AJAX-запрос данных для вывода на страницу if(context->request->AJAXRequest()) { return(OnUpdate(context,server,path,manager)); } return(server->PageProcess(context, L"templates\\edit.tpl", this, TW_PAGEPROCESS_NOCACHE)); }
3. Реализуйте метод OnUpdate
.
//+------------------------------------------------------------------+ //| Сохранение записи в таблице HELLOWORLD | //+------------------------------------------------------------------+ TWRESULT CPageEdit::OnUpdate(const Context *context, IServer *server, const wchar_t *path, CHelloWorldManager *manager) { TWRESULT res=RES_S_OK; INT64 id =0; //--- проверки if(context==NULL || path==NULL || manager==NULL) ReturnError(RES_E_INVALID_ARGS); if(context->request==NULL) ReturnError(RES_E_INVALID_CONTEXT); //--- заполняем поля StringCchCopy(m_record.name, _countof(m_record.name), context->request->GetString(IRequest::POST,L"name")); //--- сохраняем if(RES_FAILED(res=manager->InfoUpdate(context,&m_record))) ReturnErrorExt(res,context,"failed to update record"); //--- return(RES_S_OK); }
Правила маршрутизации
1. В модуль к существующим добавьте два новых правила маршрутизации: одно - на удаление записей, второе - на добавление/редактирование записей.
//+------------------------------------------------------------------+ //| Роутинг по URL | //+------------------------------------------------------------------+ TWRESULT CHelloWorldModule::ProcessPage(const Context *context, IServer *server, const wchar_t *path) { if(context==NULL || path==NULL) ReturnError(RES_E_INVALID_ARGS); //--- if(PathCompare(L"index",path)) return(CPageIndex().Process(context,m_server,path)); if(PathCompare(L"number_one",path)) return(CPageNumberOne().Process(context,m_server,path)); if(PathCompare(L"number_two/delete",path)) return(CPageNumberTwo().OnDelete(context,m_server,path,&m_manager)); if(PathCompare(L"number_two/edit",path)) return(CPageEdit().Process(context,m_server,path,&m_manager)); if(PathCompare(L"number_two",path)) return(CPageNumberTwo().Process(context,m_server,path,&m_manager)); //--- по умолчанию return(CPageIndex().Process(context,m_server,path)); }
Обратите внимание, что, выполняя проверки, мы продвигаемся от частного к общему. Т.е. если бы у нас сначала проверялся путь number_two
, а затем number_two/delete
и number_two/edit
, то на number_two
проверка бы уже закончилась, и дальнейшей проверки путей не осуществлялось бы. Имейте это в виду.
2. Подключите новую страницу в модуль.
//+------------------------------------------------------------------+ //| TeamWox | //| Copyright © 2006-2008, MetaQuotes Software Corp. | //| https://www.metaquotes.net | //+------------------------------------------------------------------+ #include "stdafx.h" #include "HelloWorldModule.h" #include "Pages\PageIndex.h" #include "Pages\PageNumberOne.h" #include "Pages\PageNumberTwo.h" #include "Pages\PageEdit.h"
Пользовательский интерфейс
Теперь нам нужно подготовить пользовательский интерфейс для управления данными. В странице PageNumberTwo
мы добавим элементы управления, вызывающие методы C.R.U.D., а также нумератор - элемент управления, отображающий вкладки с номерами страниц под таблицей с данными. В страницу PageEdit
мы добавим редактор записей.
Страница PageNumberTwo: отображение, создание и удаление записей
1. Добавьте новый элемент управления PageNumerator
.
//--- Создание таблицы и заполнение ее данными var table = TeamWox.Control("ViewTable",table_cfg.id,table_cfg.header, RecordToTable(<tw:info_list/>)) .AddHandler(top.TableHandlers.Ordering); var pages = TeamWox.Control('PageNumerator',true,<tw:item_from/>,<tw:items_total/>,<tw:items_per_page/>) .Append('onchange',PageNumeratorChanged);
Первым его аргументом является флаг местоположения снизу от границы таблицы. Второй аргумент (токен item_from
) - порядковый номер первого элемента на странице. Третий аргумент (токен items_total
) - общее количество элементов в таблице. Последний аргумент (токен items_per_page
) - количество отображаемых элементов на странице. Как видите, три последних параметра заданы в виде пользовательских токенов, которые мы реализуем ниже.
2. Методом Append
добавляется обработчик на событие. В данном случае, событие onchange
мы обрабатываем с помощью функции PageNumeratorChanged
:
function PageNumeratorChanged(startFrom,perPage) { TeamWox.Ajax.get('/helloworld/number_two/',{from:startFrom}, { onready:function (text) { var data; data = TeamWox.Ajax.json(text); table.SetData(RecordToTable(data)); pages.Show(startFrom,pages.Total(),perPage); }, onerror:function (status) { alert(status); } }); }
Метод TeamWox.Ajax.get
отправляет данные на сервер фоновым запросом методом GET
. Первый параметр - это URL, на который идет отправка. Второй - параметры для отправки. Имена ключей будут использованы как параметры, значения - как их значения. Третий - объект, используемый для обратных вызовов.
Этот объект, в свою очередь, создается с помощью метода TeamWox.Ajax.json
, который переводит строку в формате JSON в объект и наоборот. Если передается строка, метод пытается ее распознать как JSON объект, если передан объект - он кодируется в строку.
3. В CPageNumberTwo::Tag
реализуйте токены item_from
, items_total
и items_per_page
:
//--- if(TagCompare(L"item_from",tag)) { StringCchPrintf(tmp,_countof(tmp),L"%d",m_info_current); context->response->Write(tmp); return(false); }
//--- if(TagCompare(L"items_total",tag)) { StringCchPrintf(tmp,_countof(tmp),L"%d",m_info_total); context->response->Write(tmp); return(false); }
//--- if(TagCompare(L"items_per_page",tag)) { StringCchPrintf(tmp,_countof(tmp),L"%d",_countof(m_info)); context->response->Write(tmp); return(false); }
4. В шапку страницы добавьте команду создания новой записи.
//+----------------------------------------------+ //| Шапка страницы | //+----------------------------------------------+ var header = TeamWox.Control("PageHeader","#41633C") .Command("<lngj:MENU_HELLOWORLD_LIST>","/helloworld/index", "<lngj:MENU_HELLOWORLD_LIST>") .Command("<lngj:MENU_HELLOWORLD_NEW>", "/helloworld/number_two/edit/","<lngj:MENU_HELLOWORLD_NEW_DESCR>") .Help("/helloworld/index") .Search(65536);
5. В таблицу добавьте панель с двумя кнопками - редактирования и удаления, а также обработчик их нажатия.
//--- Функция записи данных из менеджера в таблицу (массив) function RecordToTable(data) { var records = []; for(var i in data) { //--- Записываем данные в массив records records.push([ {id:'number', content:data[i].id}, {id:'name', content:data[i].name,toolbar:[ ['edit',top.Toolbar.Edit], ['delete',top.Toolbar.Delete] ]} ]); } //--- return records; } //--- Создание таблицы и заполнение ее данными var table = TeamWox.Control("ViewTable",table_cfg.id,table_cfg.header, RecordToTable(<tw:info_list/>)) .AddHandler(top.TableHandlers.Ordering) .AddHandler(top.TableHandlers.Toolbar) .Append('ontoolbar',ToolbarCommand); var pages = TeamWox.Control('PageNumerator',true,<tw:item_from/>,<tw:items_total/>,<tw:items_per_page/>) .Append('onchange',PageNumeratorChanged);
6. Функция ToolbarCommand
при наступлении события ondelete
будет открывать заданный в маршрутизации URL, который будет вызывать функцию удаления записи OnDelete
.
function ToolbarCommand(id,data) { switch(id) { case 'delete': TeamWox.Ajax.post('/helloworld/number_two/delete',{id:data[0].content,from:pages.Item()},false, { onready:function (text) { var data; data = TeamWox.Ajax.json(text); table.SetData(RecordToTable(data.data)); pages.Show(data.from,data.count,pages.PerPage()) }, onerror:function (status) { alert(status); } }); break; case 'edit': document.location = '/helloworld/number_two/edit/'+data[0].content; break; } }
Страница PageEdit: редактирование записей
1. Создайте в проекте шаблон edit.tpl
для страницы PageEdit
, добавьте в него необходимые HTML-теги и подключите библиотеку TeamWox для формирования базового окружения.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <link href="<tws:filestamp path='/res/style.css' />" rel="stylesheet" type="text/css" /> </head> <body> <script type="text/javascript"> top.TeamWox.Start(window); </script> </body> </html>
2. Создайте шапку страницы, в которой будет одна команда, возвращающая нас на страницу PageNumberTwo
.
TeamWox.Control("PageHeader","#41633C") .Command("<lngj:MENU_HELLOWORLD_LIST>","/helloworld/number_two","<lngj:MENU_HELLOWORLD_LIST>");
3. Создайте новый объект - форму ввода.
TeamWox.Control('Form', { action:'/helloworld/number_two/edit/<tw:id />', method:'post', type:'table', buttons:'save', items:[ [TeamWox.Control('Label','Name','name'),TeamWox.Control('Input','text','name','<tw:name />')] ] }).Style({'width':'500px','margin':'5px'}) .Append('onformsend',function (){document.location='/helloworld/number_two/'}) .Append('onformerror',function (){alert('Network error');});
Это элемент управления с табличным типом отображения (type: 'table'
) и кнопкой (buttons: 'save'
), которая методом POST (method: 'post'
) отправляет HTTP-запрос по URL, указанному в параметре action
.
В этой таблице одна строка с двумя столбцами. В первом столбце отображается текстовая метка (тип объекта Label
) с текстом Name
, которая привязана к полю name
. Соответственно, вторым столбцом идет форма ввода (объект типа Input
) с именем text
, в которой вводится значение name
. Если это новая запись в таблице, то поле будет пустым, если редактируется существующая запись - четвертым параметром с помощью токена tw:name
в поле вставляется текущее значение записи.
При успешной отправке данных через форму (событие onformsend
) выполнится автоматический переход на страницу PageNumberTwo
. Если при отправке данных произойдет ошибка (событие onformerror
), например сбой в подключении, то браузер оповестит нас об этом стандартным методом.
4. Реализуйте использованные ранее токены id
и name
в исходном коде страницы PageEdit
.
//+------------------------------------------------------------------+ //| Обработка тэга | //+------------------------------------------------------------------+ bool CPageEdit::Tag(const Context *context,const TagInfo *tag) { wchar_t str[64]={0}; //--- проверки if(context==NULL || tag==NULL || m_server==NULL) ReturnError(false); if(context->request==NULL || context->user==NULL) ReturnError(false); //--- if(TagCompare(L"id",tag)) { if(m_record.id>0) { StringCchPrintf(str,_countof(str),L"%I64d",m_record.id); context->response->WriteSafe(str,IResponse::REPLACE_JAVASCRIPT); } return(false); } //--- if(TagCompare(L"name",tag)) { if(m_record.name!=NULL && m_record.name[0]!=NULL) { context->response->WriteSafe(m_record.name,IResponse::REPLACE_JAVASCRIPT); } return(false); } //--- return(false); } //+------------------------------------------------------------------+
Демонстрация работы пользовательского интерфейса
Мы реализовали необходимый функционал C.R.U.D. Теперь осталось проверить, как все это работает. Скомпилируйте модуль, скопируйте шаблоны на сервер и запустите TeamWox.
Добавление записей
1. Перейдите на страницу PageNumberTwo
нажмите команду создания новой записи.
2. Введите текст и нажмите кнопку сохранения.
3Запись добавилась в таблицу.
Редактирование записей
1. На странице наведите на запись курсор мыши. Появляется панель с двумя кнопками редактирования и удаления. Выберите редактирование записи.
2. Отредактируйте запись, введя новые данные.
Запись изменилась.
Нумератор
1. Добавьте в таблицу несколько записей. В массиве m_info[5]
мы указали количество записей на одну вкладку нумератора. Если записей в таблице станет больше 5, то создадутся вкладки, на каждой из которых будет выводиться максимум по 5 записей.
2. При переходе на следующую вкладку будет отображаться следующая порция данных.
Удаление записей
1. Протестируйте удаление данных. Для этого в таблице выберите кнопку удаления.
2. Запись удаляется, после чего данные в таблице автоматически обновляются.
Заключение
Итак, мы рассмотрели как с помощью средств SDK взаимодействовать с СУБД TeamWox. Статья получилась довольно объемной, поскольку в ней детально изложены изменения в исходных кодах модуля.
- Как добавить готовый модуль в TeamWox
- Как добавить страницу в модуль TeamWox
- Построение пользовательского интерфейса
- Взаимодействие с СУБД
- Создание пользовательских отчетов
- Файловое хранилище - Часть 1
- Файловое хранилище - Часть 2
- Настройка окружения пользовательских модулей - Часть 1
- Настройка окружения пользовательских модулей - Часть 2
- Поиск и фильтрация - Часть 1
- Поиск и фильтрация - Часть 2
- Настройка "Онлайн-консультанта" на вашем сайте
- Как создать дополнительный языковой пакет для TeamWox
2010.10.28