|
В этой статье :
"Толстый клиент" для работы с Web
Service.
|
"Толстый клиент"
для работы с Web Service. |
Вы скорее всего слышали что-то о "тонком" и
"толстом клиенте". В первом приближении тонкий (thin) клиент -
это программа, с которой Вы можете работать на "голой" машине,
на которой установлена только ОС и Browser. Можно
ли для нашего Web Service создать такое приложение? Конечно, да!
И очень легко. Самый простой способ - написать его на Classic ASP
или ASP.NET. Но в этой статье мы рассмотрим толстый (fat) клиент.
Данный клиент имеет преимущество: на таком компьютере можно установить
библиотеку для запуска приложения FoxPro или даже полностью FoxPro,
если это машина разработчика. |
Итак, начнем. Файл проекта
поместим в директорий C:\ws_message\client\ . Общая картина будет
примерно такой: |
|
В головном модуле задаем глобальные
переменные, производим проверку на недопустимость многократного
запуска нашего приложения. В общем, обычная несложная
рутина: |
*/-----------------------------------------------------------------------/*
*
* MS VFP version..: 9.0
* Program-ID......: MAIN.PRG
* Purpose.........: Главная программа клинета системы обмена сообщений
* Project Manager.:
* Programmer......: Sergey Chavlytko
* Start...........: 29/05/2005
* Last edited.....: 07/06/2005
*
* (С) www.sergey.co.uk 2005
*/--------------------------------------------------------------------------/*
IF _VFP.STARTMODE=0
_SCREEN.VISIBLE= .T.
* при запуске из среды разработки данная клавиша сохранит много Вашего времени
* если понадобится прервать выполнение программы.
ON KEY LABEL F12 SET SYSMENU TO DEFAULT
ELSE
_SCREEN.VISIBLE= .F.
ENDIF
* очищаем на всякий случай окружение
RELEASE ALL EXTENDED
CLEAR ALL
* делаем стандартные установки
SET CONFIRM ON
SET DELETED ON
SET EXACT OFF
SET EXCLUSIVE OFF
SET MULTILOCKS ON
SET SAFETY OFF
SET TALK OFF
SET DATE BRITISH
SET HOURS TO 24
* запоминаем номер версии программы в глобальной переменной
PUBLIC m.gcproramversion
m.gcproramversion=''
AGETFILEVERSION(AVER,'mesclient.exe')
IF TYPE('AVER')#'U'
m.gcproramversion=' (ver: ' +AVER(11)+')'
ENDIF
PUBLIC m.gltransact
m.gltransact=.f.
* проверяем наличие предыдущей копии программы в памяти
IF AppAlreadyRunning()
=MESSAGEBOX("Вы конечно извините, но запустить данную программу "+ ;
"можно только один раз...", 0 + 16 +0, ;
"Ошибка в течении старта программы")
QUIT
ENDIF
* создаем переменные путей для создания временных таблиц и курсоров
* в принципе сейчас они не нужны, но это хорошая привычка все указывать
* явно и мы будем ей следовать...
PUBLIC m.gctmp
m.gctmp=SYS(2023)+'\'
PUBLIC m.gccurdir
m.gccurdir=SYS(5)+SYS(2003)+'\'
* явно определяем установки среды, которые будут видны во всей программе
* обычно эту информацию следует записывать во внешний DBF файл конфигурации
* если Вы все повторяете за нами все, то скорее всего Вам не надо будет менять
* имя Web Service (localhost - это только для Вашей локальной машины)
* если Вы будете использовать WS в Intranet - то замените на имя Вашей машины
* (например SERGEY04)
* при использовании в Internet - на реальный адрес...
*
PUBLIC gcwebserv
gcwebserv='http://localhost/ws_server/ws_mes_server.wsdl'
* объявляем глобальные переменные, которые потом будем использовать в программе
* мы будем использовать этот вариант для простоты, хотя в последнее время реко-
* мендуют создавать глобальный объект приложение и менять его свойства...
PUBLIC m.gclog, m.gcpsw, m.gcadmin,m.gnuser_id
STORE SPACE(10) TO m.gclog, m.gcpsw
m.gcadmin=0
m.gnuser_id=0
DO FORM frmmain
READ EVENTS
QUIT
* функция избежания повтороного запуска программы
FUNCTION AppAlreadyRunning
LOCAL hsem, lpszSemName
#DEFINE ERROR_ALREADY_EXISTS 183
DECLARE INTEGER GetLastError IN win32API
DECLARE INTEGER CreateSemaphore IN WIN32API ;
STRING @ lpSemaphoreAttributes, ;
LONG lInitialCount, ;
LONG lMaximumCount, ;
STRING @ lpName
lpszSemName = 'MESCLIENT'
hsem = CreateSemaphore(0,0,1,lpszSemName)
RETURN (hsem # 0 AND GetLastError() == ERROR_ALREADY_EXISTS)
*
* оформим прием данных в виде универсальной процедуры
* в зависимости от передаваемых параметров меняем выполняемые
* функции на удаленном сервере
*
* m.lckey_word - имя выполняемой функции
* m.lcparameter1,m.lcparameter2 - передаваемые параметры
* m.lcNewTableName - имя создаваемого курсора (если не пустое)
*
*
*
*
PROCEDURE message_read
LPARAMETERS m.lckey_word,m.lcNewTableName,m.lcparameter1,m.lcparameter2,m.lcparameter3,;
m.lcparameter4,m.lcparameter5,m.lcparameter6
m.lcreturn=.F.
LOCAL loProxy
TRY
loProxy=CREATEOBJECT("MSSOAP.SOapClient30")
WAIT WINDOW 'I am trying to connect to web server...' NOWAIT
loProxy.MSSoapInit(gcWebServ)
loProxy.ConnectorProperty("Timeout") = 90 * 1000 && milliseconds
WAIT WINDOW 'Receiving data...' NOWAIT
lcXML=loProxy.message_read(m.lckey_word,m.gclog,m.gcpsw,m.lcparameter1,m.lcparameter2,;
m.lcparameter3,m.lcparameter4,m.lcparameter5,m.lcparameter6)
CATCH TO oErr
m.lcmess='('+ALLTRIM(STR(oErr.Errorno))+') '+TRIM(oErr.Details)
MESSAGEBOX(m.lcmess,0,'We cannot connect to your Web Server. Error: ',30000)
m.gcerror_form=SUBSTR(('Error during receiving: ('+ALLTRIM(STR(oErr.Errorno))+') '+;
TRIM(oErr.Details)),1,254)
EXIT
FINALLY
RELEASE loProxy
ENDTRY
WAIT WINDOW 'Operation has been completed...' TIMEOUT 0.1
*waIT WINDOW lcXML
*? lcXML
IF TYPE('lcXML')="C" AND LEN(lcXML)>30
XMLTOCURSOR(lcXML,m.lcNewTableName,4)
SELECT &lcNewTableName
IF RECCOUNT()>0
m.lcreturn=.T.
ELSE
m.lcreturn=.F.
ENDIF
ELSE
IF EMPTY(m.lcNewTableName) && for executive functions
IF (TYPE('lcXML')="N" AND lcXML=1) OR (TYPE('lcXML')="C" AND EVALUATE('VAL(lcXML)>=1'))
m.lcreturn=.T.
ELSE
m.lcreturn=.F.
ENDIF
ELSE
WAIT WINDOW 'We are very sorry, but we could not retrieve data from Web server ' TIMEOUT 5
ENDIF
ENDIF
RETURN m.lcreturn |
Немного заострим Ваше внимание на обработке
ошибок в процедуре message_read. Она построена на простом анализе
возвращаемого или нет параметра от удаленного Web Service. В
принципе это все можно усложнить в Вашем реальном приложении, но мне
для практических нужд такой нехитрой обработки вполне хватает. |
Далее разработаем основную
форму frmmain.scx . Тут есть небольшая хитрость. Мы хотели, чтобы
это приложение было как можно проще. Выбор пал на formset. У нас
две формы: одна для регистрации клиентов, а вторая для
непосредственно работы с данными. Если регистрация прошла неудачно, подается
команда CLEAR EVENTS, и программа заканчивает свою работу. |
|
На форме регистрации есть кнопка со
странным названием "Д" - это кнопка для разработчика (Developer).
Она съэкономит много Вашего времени - то есть при тестировании
программы Вы автоматически будете проходить процесс регистрации в
систему. Не забудьте только в методе формы INIT сделать ее видимой
только для Вас: |
* стандартный трюк, чтобы съэкономить время разработчика
IF _VFP.STARTMODE=0
THISFORM.cmdD.VISIBLE =.T.
ELSE
THISFORM.cmdD.VISIBLE =.F.
ENDIF |
Для того, чтобы наша форма была все
время наверху и активна в метод формы ACTIVATE вносим код: |
* помещаем наше окно в верхнем слое
DECLARE INTEGER SetForegroundWindow IN USER32 INTEGER
DECLARE INTEGER FindWindow IN USER32 STRING @ , STRING @
lnHWND=FindWindow(0, _Screen.Caption)
IF lnHWND>0
SetForegroundWindow(lnHWND)
ENDIF |
Метод кнопки "Войти" наиболее сложен. Здесь
уже идет вызов метода login нашего Web Service с передачей в
качестве параметров имени пользователя и пароля. В результате
получаем курсор ACCESS с некоторыми свойствами, которые мы
используем в приложении. В реальной жизни это очень удобное место
для передачи на клиент названия модулей внутри программы, к которым у клиента
есть доступ или даже передача самих модулей; удачное место для
проверки устарела ли программа клиента или нет и установки более свежей
версии (опять же передача ее через Интернет). В общем, все
ограничено только Вашей фантазией и деньгами клиента. В конце
производим установки интерфейса в зависимости от прав клиента: |
LOCAL loProxy
TRY
loProxy=CREATEOBJECT("MSSOAP.SOapClient30")
WAIT WINDOW 'Произвожу соединение с web server...' NOWAIT
loProxy.MSSoapInit(gcwebserv)
* устанавливаем время возможной задержки ответа от сервера
loProxy.ConnectorProperty("Timeout") = 90 * 1000 && milliseconds
WAIT WINDOW 'Идет прием данных...' NOWAIT
lcXML=loProxy.login(m.gclog,m.gcpsw)
CATCH TO oErr
m.lcmess='('+ALLTRIM(STR(oErr.ERRORNO))+') '+TRIM(oErr.DETAILS)
MESSAGEBOX(m.lcmess,0,'We cannot connect to your Web Server. Error: ',30000)
EXIT
FINALLY
RELEASE loProxy
ENDTRY
* теперь очень простая проверка на то, что вернулось на наш запрос
IF TYPE('lcXML')="C".AND.LEN(lcXML)>25
WAIT WINDOW 'Получаем из XML файла курсор...' NOWAIT
XMLTOCURSOR(lcXML,"Access",0)
* в реальной жизни я пищу в этот курсор имена программных модулей,
* к которым имеет доступ данный клиент - ну а пока одна строка
SELECT ACCESS
m.gnuser_id=ACCESS.User_ID
m.gcadmin=ACCESS.Admin
ELSE
=MESSAGEBOX("Нам очень жаль, но Вы не смогли зайти в систему...", 0 + 16 +0,;
"Ошибка при подключению к удаленному серверу.", 10000)
CLEAR EVENTS
ENDIF
WAIT WINDOW 'Прием данных завершен...' TIMEOUT 0.1
* делаем некоторые контролы видимыми только для администратора
IF m.gcadmin=1
thisformset.frmMAIN.cmdDeleteMessage.Visible =.t.
thisformset.frMMAIN.cmdAddUserNew.Visible = .t.
thisformset.frMMAIN.cmdEditUser.Visible = .t.
ELSE
thisformset.frmMAIN.cmdDeleteMessage.Visible =.f.
thisformset.frMMAIN.cmdAddUserNew.Visible = .f.
thisformset.frMMAIN.cmdEditUser.Visible = .f.
ENDIF
thisformset.frmLOGIN.Visible =.f.
thisformset.frmMAIN.Visible =.t.
thisformset.frMMAIN.Caption=('Система обмена сообщениями ('+alltrim(m.gclog)+')')
thisform.Release |
После успешной регистрации клиент попадает
на основную форму работы с данными. Как обычно - данных нет. Они
запрашиваются путем нажатия кнопки "Освежить данные": |
THISFORM.timer1.RESET
IF !EMPTY(m.gclog) AND !EMPTY(m.gcpsw)
IF USED('MESSAGES')
SELECT MESSAGES
m.lnMes_ID=MESSAGES.Mes_ID
SET FILTER TO
ELSE
m.lnMes_ID=0
ENDIF
IF message_read('READ_MESSAGES_START','MESSAGES',DATE(),THISFORM.spnDaysUpdate.VALUE)
THISFORM.grid1.RECORDSOURCE=''
THISFORM.grid1.RECORDSOURCE='MESSAGES'
THISFORM.grid1.INIT()
SELECT MESSAGES
IF !EMPTY(m.lnMes_ID)
LOCATE FOR m.lnMes_ID=MESSAGES.Mes_ID
ELSE
GOTO BOTTOM
ENDIF
thisformset.counter=thisformset.counter+1
THISFORM.lblmessage.CAPTION = 'Последнее обновление с сервера: '+;
TTOC(DATETIME())+' ('+ALLTRIM(STR(thisformset.counter))+')'
ELSE
THISFORM.grid1.RECORDSOURCE=''
ENDIF
ENDIF
THISFORM.grid1.REFRESH()
THISFORM.grid1.SETFOCUS()
THISFORM.timer1.RESET |
Ничего сложного - некоторые нюансы только с
установками Grid, так как мы после приема данных заново воссоздаем
курсор MESSAGES. Основную работу мы возложили на метод INIT в нашем
Grid (не забывая при этом каждый раз очищать DataSource в основной
программе вызова): |
*
* пробуем построить красивый Grid
*
IF USED('MESSAGES') && на всякий случай проверим наличие открытого курсора
THIS.READONLY= .T.
THIS.DELETEMARK= .F.
THIS.SCROLLBARS= 2
THIS.GRIDLINES= 0
*!* THIS.FONTSIZE= 9
*!* THIS.ROWHEIGHT = 20
THIS.COLUMNCOUNT = 6
THIS.ALLOWCELLSELECTION = .F.
WITH THIS.column1
.ALIGNMENT=1
.WIDTH=60
.CONTROLSOURCE="MESSAGES.Mes_ID"
.header1.CAPTION='Сообщ. No'
.header1.ALIGNMENT=2
.READONLY=.T.
ENDWITH
WITH THIS.column2
.WIDTH=70
.ALIGNMENT=0
.CONTROLSOURCE='MESSAGES.User_nick'
.header1.CAPTION='Автор'
.header1.ALIGNMENT=2
.READONLY=.T.
ENDWITH
WITH THIS.column3
.WIDTH=100
.ALIGNMENT=0
.CONTROLSOURCE='MESSAGES.city'
.header1.CAPTION='Откуда'
.header1.ALIGNMENT=2
.READONLY=.T.
ENDWITH
WITH THIS.column4
.ALIGNMENT=0
.WIDTH=100
.FONTSIZE=8
.CONTROLSOURCE="MESSAGES.published"
.header1.CAPTION='Опубликовано'
.header1.ALIGNMENT=2
ENDWITH
WITH THIS.column5
.ALIGNMENT=0
.WIDTH=100
.FONTSIZE=8
.CONTROLSOURCE="MESSAGES.WASUPDATED"
.header1.CAPTION='Изменено'
.header1.ALIGNMENT=2
ENDWITH
WITH THIS.column6
.ALIGNMENT=0
.WIDTH=380
.CONTROLSOURCE="MESSAGES.TITLE"
.header1.CAPTION='Тема'
.header1.ALIGNMENT=2
.READONLY=.T.
ENDWITH
* раскрасим немного строки
THISFORM.grid1.SETALL("DynamicBACKColor", ;
"IIF(MESSAGES.User_to=m.gnuser_id , RGB(185,255,185),"+;
"IIF(MESSAGES.User_from=m.gnuser_id,RGB(217,217,255),RGB(255,255,255)) )", "Column")
* поощрим желание некоторых клиентов прочитать сообщение по двойному нажатии
* левой клавиши мышки (а заодно и по правой клавише)
BINDEVENT(THIS,"Dblclick",THISFORMSET,"Read_Message",1)
BINDEVENT(THIS,"Rightclick",THISFORMSET,"Read_Message",1)
ENDIF |
Все строим "вручную". Может быть это и не
очень эффективный прием программирования, но зато наглядный. Очень
полезна команда BINDEVENT, появившаяся в VFP 8.0 - она одной строкой
позволяет делать "фантастические вещи". Единственное уточнение -
метод "Read_Message" создаем в главной FormSet. |
Для автоматического обновления данных
мы используем таймер, параметры которого задают наши
клиенты самостоятельно. Думаю, что они будут довольны, получив
некоторый контроль над событиями. При вызове дополнительных форм надо
не забывать его сбрасывать: для этого мы используем свойство главного
FormSet -appbusy. Все очень просто. |
Из "вспомогательных" форм
рассмотрим добавление нового пользователя администратором.
После непродолжительных раздумий мы решили лишить клиентов
возможности самим регистрироваться, но если Вы не согласны, то можете изменить
программу так, как считаете нужным. |
|
В общем-то стандартная форма ввода данных.
Интерес для нас представляет метод Click() кнопки
"Сохранить". |
IF THISFORM.newmessage=.T.
IF EMPTY(THISFORM.txtUser_nick.VALUE)
MESSAGEBOX("Псевдоним(Login)","Пожалуйста, заполните следующее поле",64,3000)
THISFORM.txtUser_nick.SETFOCUS
RETURN
ENDIF
IF EMPTY(THISFORM.txtPassword.VALUE)
MESSAGEBOX("Пароль.",;
"Пожалуйста, заполните следующее поле",64,3000)
THISFORM.txtPassword.SETFOCUS
RETURN
ENDIF
* для простоты дальнейшего развития системы просто передаем весь курсор
* данный пример показывает как передавать на сервер курсор FoxPro
* все как обычно - в виде символьной строки (XML)
IF USED('USERS_PROF')
SELECT USERS_PROF
CURSORTOXML("USERS_PROF","lcXML",1,1,0,"1")
ELSE
lcXML=""
ENDIF
IF message_read('ADD_NEW_USER','',(THISFORM.text2.VALUE),lcXML)
THISFORM.RELEASE
ELSE
* в случае неудачи - просто снова возвращаемся в экран ввода нового сообщения
=MESSAGEBOX("Нам очень жаль, но Вы не смогли опубликовать данные...", 0 + 16 +0,;
"Ошибка при подключению к удаленному серверу.", 10000)
ENDIF
ELSE
IF EMPTY(THISFORM.txtUser_nick.VALUE)
MESSAGEBOX("Псевдоним(Login)","Пожалуйста, заполните следующее поле",64,3000)
THISFORM.txtUser_nick.SETFOCUS
RETURN
ENDIF
IF EMPTY(THISFORM.txtPassword.VALUE)
MESSAGEBOX("Пароль.",;
"Пожалуйста, заполните следующее поле",64,3000)
THISFORM.txtPassword.SETFOCUS
RETURN
ENDIF
* для простоты дальнейшего развития системы просто передаем весь курсор
* данный пример показывает как передавать на сервер курсор FoxPro
* все как обычно - в виде символьной строки (XML)
IF USED('USERS_PROF')
SELECT USERS_PROF
CURSORTOXML("USERS_PROF","lcXML",1,1,0,"1")
ELSE
lcXML=""
ENDIF
IF message_read('ADD_NEW_USER','',(THISFORM.text2.VALUE),lcXML)
THISFORM.RELEASE
ELSE
* в случае неудачи - просто снова возвращаемся в экран ввода нового сообщения
=MESSAGEBOX("Нам очень жаль, но Вы не смогли опубликовать данные...", 0 + 16 +0,;
"Ошибка при подключению к удаленному серверу.", 10000)
ENDIF
ENDIF |
Часть проверок мы все-таки производим на
стороне клиента, чтобы не загружать наш сервер лишней работой. В
этом примере показано, как передать на сервер курсор. По умолчанию
его размер не превышает 100 Kb. Для нас этого хватит, но на практике
надо увеличить его размер. Делается это путем нехитрых манипуляций с
реестром Windows: |
1. Запустим программу для редактирования реестра: |
|
2. Найдем в реестре вхождение для ключа
SOAPISAP: |
|
3. Отредактируем значение для ключа
MaxPostSize (по умолчанию это значение около 102 Kb. Имейте ввиду,
что это не размер передаваемого Вами файла, в котором будут
присутствовать XML тэги, так что размер Вашего передаваемого файла будет много меньше этого значения). |
|
Разумное значение для быстрого Интернета (512
Kb/s) считается в пределах 4-8 Mbytes... |
|
Остальные параметры мы
рекомендуем оставить по умолчанию. Есть еще один нюанс - при расшифровке
принятого XML файла в настоящее время тратится очень много
компьютерных ресурсов (поэтому нами рекомендуется передавать данные сразу в виде
таблиц или текста с разделителями, "обвернутых" в поле MEMO файла
XML). Кроме того, делите большие файлы на части и передавайте их по
частям, поверьте, так выйдет быстрее. |
|
Скачать исходные тексты для Клиента можно
отсюда (файл
ws_mes_client.zip 72 Kb) В архив включен даже откомпилированный
файл приложения. |
|
|
|