Статья: Web Services и MS Visual FoxPro Часть 5

В этой статье :

  • "Толстый клиент" для работы с 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 тэги, так что размер Вашего передаваемого файла будет много меньше этого значения).
    Ключи, относящиеся к Web Service.

    Разумное значение для быстрого Интернета (512 Kb/s) считается в пределах 4-8 Mbytes...

    Непосредственно экран редактирования, вызываемый правой кнопкой мышки меню из которого выбираем Modify.
    Остальные параметры мы рекомендуем оставить по умолчанию. Есть еще один нюанс - при расшифровке принятого XML файла в настоящее время тратится очень много компьютерных ресурсов (поэтому нами рекомендуется передавать данные сразу в виде таблиц или текста с разделителями, "обвернутых" в поле MEMO файла XML). Кроме того, делите большие файлы на части и передавайте их по частям, поверьте, так выйдет быстрее.
    Скачать исходные тексты для Клиента можно отсюда (файл ws_mes_client.zip 72 Kb) В архив включен даже откомпилированный файл приложения.