Andrey on .NET | Создание WCF сервиса с поддержкой Ajax: ч.2 – Клиент

Создание WCF сервиса с поддержкой Ajax: ч.2 – Клиент

В первой части был разработан WCF сервис для получения данных из каталога веб-сайтов. Теперь приступим к разработке клиента.

Разработка клиентского приложения с применением технологии Ajax

Удаленный прокси для клиента

Для начала вернемся к странице WebsiteDataService.svc. Если открыть её в браузере и добавить в конец адреса параметр /jsdebug, то результатом будет вот такой JavaScript файл:

Type.registerNamespace('WebsiteCatalogService');
WebsiteCatalogService.IWebsiteDataService=function() {
WebsiteCatalogService.IWebsiteDataService.initializeBase(this);
this._timeout = 0;
this._userContext = null;
this._succeeded = null;
this._failed = null;
}
WebsiteCatalogService.IWebsiteDataService.prototype={
_get_path:function() {
 var p = this.get_path();
 if (p) return p;
 else return WebsiteCatalogService.IWebsiteDataService._staticInstance.get_path();},
Insert:function(websiteInfo,succeededCallback, failedCallback, userContext) {
/// <param name="websiteInfo" type="WebsitesCatalog.WebsiteInfo">WebsitesCatalog.WebsiteInfo</param>
/// <param name="succeededCallback" type="Function" optional="true" mayBeNull="true"></param>
/// <param name="failedCallback" type="Function" optional="true" mayBeNull="true"></param>
/// <param name="userContext" optional="true" mayBeNull="true"></param>
return this._invoke(this._get_path(), 'Insert',false,{websiteInfo:websiteInfo},succeededCallback,failedCallback,userContext); },
Delete:function(id,succeededCallback, failedCallback, userContext) {
/// <param name="id" type="String">System.String</param>
/// <param name="succeededCallback" type="Function" optional="true" mayBeNull="true"></param>
/// <param name="failedCallback" type="Function" optional="true" mayBeNull="true"></param>
/// <param name="userContext" optional="true" mayBeNull="true"></param>
return this._invoke(this._get_path(), 'Delete',false,{id:id},succeededCallback,failedCallback,userContext); },
Get:function(id,succeededCallback, failedCallback, userContext) {
/// <param name="id" type="String">System.String</param>
/// <param name="succeededCallback" type="Function" optional="true" mayBeNull="true"></param>
/// <param name="failedCallback" type="Function" optional="true" mayBeNull="true"></param>
/// <param name="userContext" optional="true" mayBeNull="true"></param>
return this._invoke(this._get_path(), 'Get',false,{id:id},succeededCallback,failedCallback,userContext); }}
WebsiteCatalogService.IWebsiteDataService.registerClass('WebsiteCatalogService.IWebsiteDataService',Sys.Net.WebServiceProxy);

/// [ удалено для более краткого изложения ]

var gtc = Sys.Net.WebServiceProxy._generateTypedConstructor;
Type.registerNamespace('WebsitesCatalog');
if (typeof(WebsitesCatalog.WebsiteInfo) === 'undefined') {
WebsitesCatalog.WebsiteInfo=gtc("WebsiteInfo:http://schemas.datacontract.org/2004/07/WebsitesCatalog");
WebsitesCatalog..registerClass('WebsitesCatalog.WebsiteInfo');
}

Часть файла убрана для краткости. Даже если бегло посмотреть на данный код, то заметны знакомые название классов и методов. На самом деле это не что иное как Прокси для разработанного WCF сервиса.

Созданная автоматически реализация находится в указанном при описании контракта пространстве имен WebsiteCatalogService. Обратите внимание, что используется именно оно, а не пространство имен самого приложения. В качестве имени класса используется имя интерфейса IWebsiteDataService, который определяет контракт. Для хранения полученных данных зарегистрирован еще один класс – WebsiteInfo.

Таким образом, WCF сервис предоставляет возможность обращаться к нему с ASP.NET страниц.

JavaScript для взаимодействия с WCF

Перейдем к разработке клиентский части. В корневую папку проекта добавим новый файл WebsitesCatalog.js. Это и будет код взаимодействия с WCF сервисом. Его задачей является получение описания для заданного сайта. Давайте посмотрим на исходный код:

Type.registerNamespace("WebsitesCatalog");

WebsitesCatalog.WebsiteDetails = function (descriptionElementId) {
    WebsitesCatalog.WebsiteDetails.initializeBase(this);
    this._descriptionElement = document.getElementById(descriptionElementId);
}

WebsitesCatalog.WebsiteDetails.prototype =
{
    ShowDescription: function (websiteId) {
        WebsiteCatalogService.IWebsiteDataService.Get(websiteId,
          Function.createDelegate(this, this.OnCompleted), this.OnError);
    },

    OnCompleted: function (result, userContext, methodName) {
        if (result == "") { return; }
        if (result.Id == "") { return; }

        this._descriptionElement.innerHTML = result.Description;
    },

    OnError: function (result, userContext, methodName) {
        var msg = result.get_exceptionType() + "\r\n";
        msg += result.get_message() + "\r\n";
        msg += result.get_stackTrace();
        alert(msg);
    }
}

WebsitesCatalog.WebsiteDetails.registerClass("WebsitesCatalog.WebsiteDetails");

Давайте детально рассмотрим созданный класс WebsiteDetails. Его конструктор получает Id элемента страницы, который будет использоваться для вывода описания выбранного сайта.

Функция ShowDescription() запрашивает с сервера описание сайта по заданному websiteId. Для этого она обращается к WCF сервису, используя метод Get(). Точнее сказать что запрос направляется Прокси, который уже пересылает его на сервер. При этом используется асинхронный подход к взаимодействию, поэтому в качестве параметров указаны функции, которые будут отвечать за обработку ответа.

Функция OnCompleted() при удачном завершении асинхронного вызова получит данные выбранного сайта. После этого проверяется значение Id и если оно не пустое (т.е. сайт был найден в базе), то описание сайта записывается в заданный элемент страницы.

В случае сбоя вызова будет вызвана функция OnError() для вывода сообщения об ошибке.

Создаем ASP.NET страницу

Осталось добавить веб-страницу с именем Default.aspx также в корневой каталог сайта. Чтобы не отвлекаться от основной задачи, упростим ее на сколько это возможно. Добавим все ссылки из базы в виде текста, а для описания выбранного сайта выделим область под ним.

Файл разметки будет выглядеть следующим образом:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebsitesCatalog.Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title>Demo</title>
    <script type="text/javascript">
        var websiteCatalog = null;

        function OnPageLoad() {
            websiteCatalog = new WebsitesCatalog.WebsiteDetails('_websiteDescription');
        }
    </script>
</head>
<body onload="OnPageLoad();">
    <form id="form1" runat="server">
    <asp:ScriptManager ID="_scriptManager" runat="server">
        <Services>
            <asp:ServiceReference Path="~/Service/WebsiteDataService.svc" />
        </Services>
        <Scripts>
            <asp:ScriptReference Path="~/WebsitesCatalog.js" ScriptMode="Auto" />
        </Scripts>
    </asp:ScriptManager>
    <div>
        Websites:<br />
        <asp:Literal ID="_websitesList" runat="server" /><br />
        Description:<br />
        <asp:Label ID="_websiteDescription" runat="server" Text="" />
        <hr />
        <table>
            <tr>
                <td>Title:</td>
                <td><asp:TextBox ID="_siteTitle" runat="server" /></td>
            </tr>
            <tr>
                <td>Url:</td>
                <td><asp:TextBox ID="_siteUrl" runat="server" /></td>
            </tr>
            <tr>
                <td>Description:</td>
                <td>
                    <asp:TextBox ID="_siteDescription" runat="server" TextMode="MultiLine" />
                </td>
            </tr>
        </table>
        <asp:Button ID="_btnAddSite" runat="server" Text="Add" OnClick="AddSite_Click" />
    </div>
    </form>
</body>
</html>

Обратите внимание как подключаются скрипты. Они указаны в элементе <asp:ScriptManager> в соответствующих разделах. В <Services> задаются скрипты сервиса, а в <Scripts> – пользовательские.

При загрузке страницы срабатывает скрипт, создающий экземпляр класса WebsiteDetails.

В остальном страница вполне обычная. Она содержит место для вывода списка сайтов _websitesList, выбранного описания _websiteDescription, а также таблицу для добавления нового сайта в базу.

Перейдем к C# коду этой страницы. В нем содержится 2 метода:

  1. В методе Page_Load() выводится список сайтов. Для каждого из низ создается ссылка "[info]", при нажатии на которую срабатывает JavaScript. Он обращается к экземпляру WebsiteDetails и запрашивает данные о сайте. В готовом HTML коде это будет выглядеть так:
    <a href="http://andrey.moveax.ru">Andrey on .NET</a> | <a href="javascript:websiteCatalog.ShowDescription('eacd4df4-f2fd-430a-9a88-4f353d5d73ff');">[info]</a>
  2. Метод AddSite_Click() добавляет новый сайт в каталог. Метод упрощен. В нем отсутствуют проверки на корректность введенных данных.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace WebsitesCatalog
{
    public partial class Default : System.Web.UI.Page
    {
        private readonly WebsiteDataSource _dataSource = new WebsiteDataSource();

        protected void Page_Load(object sender, EventArgs e)
        {
            foreach (WebsiteInfo websiteInfo in this._dataSource.Items) {
                this._websitesList.Text += string.Format(
                    "<a href=\"{0}\">{1}</a> | <a href=\"javascript:websiteCatalog.ShowDescription('{2}');\">[info]</a><br />",
                    websiteInfo.Url, 
                    websiteInfo.Title,
                    websiteInfo.Id);
            }
        }

        protected void AddSite_Click(object sender, EventArgs e)
        {
            WebsiteInfo websiteInfo = new WebsiteInfo() {
                Id = Guid.NewGuid().ToString(),
                Title = this._siteTitle.Text,
                Url = this._siteUrl.Text,
                Description = this._siteDescription.Text
            };

            this._dataSource.AddOrUpdate(websiteInfo);
            Response.Redirect("/Default.aspx");
        }
    }
}

Проект завершен и готов к запуску.

И как всегда, в завершении ссылка на проект для Visual Studio 2010:
WebsitesCatalog.zip (16.6 kb).

Комментарии (12) -

Огромное спасибо за труд, очень помог понять, как начать работать с WCF.
В данной статье в методах Page_Load и _btnAddSite_Click вместо Title надо Name писать.

@ Porter: Спасибо и вам за отзыв. По поводу ошибки: постоянно дорабатываю примеры перед публикацией и вот случается. На самом деле опечатка в первой части - там Title. Но сути это не меняет.

Constantine 03.01.2011 21:53:00

Спасибо, только много ошибок. Проект по ссылке рабочий, попытка создать подобное приложение не увенчалась успехом. Буквально вот на этой странице

<asp:Button ID="_btnAddSite" runat="server" Text="Add" OnClick="AddSite_Click" />
protected void _btnAddSite_Click(object sender, EventArgs e)
имена методов различаются.
JS файл или Default.aspx тоже с ошибкой - сервис не работает.

"Если открыть её в браузере и добавить в конец адреса параметр /jsdebug, то результатом будет вот такой JavaScript файл:" - на самом деле namespace - "tempuri.org"

С именем метода – опечатка, исправил.

Далее, по JS – вы не указали namespace в IWebsiteDataService.cs. Там есть строка:

[ServiceContract(Namespace = "WebsiteCatalogService")]

JS файл или Default.aspx тоже с ошибкой - сервис не работает.

А вот тут подробнее. Что именно не работает. Возможно проблема из-за неуказанного Namespace.

вот такая ошибочка вышла при попытке открыть браузером файл WebsiteDataService.svc. Вот строка запроса: http://localhost:2863/WebsitesCatalog/Service/WebsiteDataService.svc/jsdebug

"Не удается найти тип "WebsitesCatalog.WebsiteDataService", предоставленный в директиве ServiceHost в качестве значения атрибута Service." Т.е. по факту мой каталог вебсайта не виден. на сколько я понял. Только вот не пойму почему. Правда у меня Visual Studio 2008 стоит, может она не все понимает.

@ Haldey:
http://localhost:[port]/Service/WebsiteDataService.svc
откуда взяли еще /WebsitesCatalog/Service

Алексей 07.10.2017 23:08:11

Спасибо за статью!
В скрипте WebsitesCatalog.js мы создаём пространство имен WebsitesCatalog, а в методе ShowDescription прототипа пробуем вызвать метод Get интерфейса в пространстве имен WebsitesCatalogService - наверное опечатка. Исправил у себя, иначе клик на [info] не отрабатывал.

Алексей я сейчас проверил пример без изменений - работает. А вы исправили как я понял вот эту строку Type.registerNamespace("WebsitesCatalog");

Алексей 09.10.2017 7:59:25

Андрей,
Я изменил только в этом методе ‘WebsiteCatalogService’, на ‘WebsiteCatalog’ -

ShowDescription: function (websiteId) {
WebsiteCatalog.IWebsiteDataService.Get(websiteId,

    },

А использовали свой код или из статьи. Ведь в примере namespace задается с помощью аргумента [ServiceContract(Namespace = "WebsiteCatalogService")]

Алексей 09.10.2017 22:23:08

Код использовал из статьи.
Проект собрал на VS Comunity 2017 for Mac.
Web.config немного изменил:
  <system.serviceModel>
    <services>
      <service behaviorConfiguration="WebsitesCatalog.WebsiteDataServiceBehavior"
                name="WebsitesCatalog.WebsiteDataService" >
        <endpoint address=""
                    behaviorConfiguration="WebsitesCatalog.WebsiteDataServiceEndpointBehavior"
                binding="webHttpBinding"
                contract="WebsitesCatalog.IWebsiteDataService" />
      </service>
    </services>  
    <behaviors>
      <serviceBehaviors>
        <behavior name="WebsitesCatalog.WebsiteDataServiceBehavior">
        </behavior>
      </serviceBehaviors>
            
      <endpointBehaviors>
        <behavior name="WebsitesCatalog.WebsiteDataServiceEndpointBehavior">
          <enableWebScript />
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />      
  </system.serviceModel>

Странно. Возможно какой-то баг в реализации под Mac (я еще раз проверил код но правда под Windows - все ок). Ведь namespace переопределен атрибутом, но судя по сказанному выше, он игнорируется.

Pingbacks and trackbacks (2)+

Добавить комментарий