Статьи

Серверные элементы управления ASP.NET AJAX с функциональностью на стороне клиента

В этом руководстве мы рассмотрим, как создать пользовательский серверный элемент управления ASP.NET AJAX в качестве оболочки для JavaScript API Карт Google. Код на стороне сервера будет написан на C # (что я очень рекомендую), но его также легко можно написать на VB.NET. Основное внимание будет уделено созданию элемента управления, и мы подробно рассмотрим весь процесс создания настраиваемого серверного элемента управления ASP.NET AJAX, а также функциональности на стороне клиента.


Если у вас еще не установлена ​​Visual Studio, вам нужно взять последнюю экспресс-версию .

Как только это будет сделано, запустите его и перейдите в File -> New -> Project. В списке слева выберите «Веб», а затем выберите «ASP.NET AJAX Server Control» на главной панели справа. Назовите проект MapControl и убедитесь, что выбран вариант создания нового решения (если применимо). Нажмите OK, чтобы создать проект.

Посмотрев в Solution Explorer, вы заметите, что Visual Studio уже сгенерировала некоторые файлы для нас. Мы немного разберем сгенерированный код, но перед этим давайте переименуем файлы и классы, содержащиеся в файлах.

  • Переименуйте ClientControl1.js в GoogleMap.js
  • Переименуйте ClientControl1.resx в GoogleMap.resx
  • Переименуйте ServerControl1.cs в GoogleMap.cs

Теперь нажмите Control + H, чтобы открыть окно быстрой замены. Выберите соответствующие параметры, чтобы заменить ServerControl1 на GoogleMap . Убедитесь, что он настроен на просмотр всего проекта, а не только текущего файла, а затем нажмите «Заменить все». Теперь сделайте то же самое, чтобы заменить ClientControl1 на GoogleMap .


Давайте разберем GoogleMap.js по частям.

1
<reference name=»MicrosoftAjax.js»/>

Эта строка просто сообщает движку IntelliSense Visual Studio о включении типов и методов, содержащихся в MicrosoftAjax.js в раскрывающиеся списки IntelliSense.

1
2
3
4
5
Type.registerNamespace(«MapControl»);
 
MapControl.GoogleMap = function(element) {
    MapControl.GoogleMap.initializeBase(this, [element]);
}

Первая строка регистрирует пространство имен MapControl с каркасом AJAX. Остальная часть кода здесь действует как конструктор для клиентского класса нашего пользовательского элемента управления. Здесь мы объявим все частные свойства на стороне клиента.

01
02
03
04
05
06
07
08
09
10
11
MapControl.GoogleMap.prototype = {
    initialize: function() {
        MapControl.GoogleMap.callBaseMethod(this, ‘initialize’);
         
        // Add custom initialization here
    },
    dispose: function() {
        //Add custom dispose actions here
        MapControl.GoogleMap.callBaseMethod(this, ‘dispose’);
    }
}

Здесь класс клиента нашего пользовательского элемента управления определяется с использованием модели прототипа. Это где все методы для класса клиента объявлены. Как вы, вероятно, догадались, методы initialize и dispose вызываются автоматически при создании и уничтожении экземпляра этого класса соответственно. В этом случае мы будем использовать метод initialize для вызова API Карт Google и настройки карты.

1
2
3
MapControl.GoogleMap.registerClass(‘MapControl.GoogleMap’, Sys.UI.Control);
 
if (typeof(Sys) !== ‘undefined’) Sys.Application.notifyScriptLoaded();

Первая часть здесь регистрирует класс с определенным именем класса и пространством имен, а также назначает базовый класс (Sys.UI.Control). Наконец, вызывается метод Sys.Application.notifyScriptLoaded (), который уведомляет платформу Microsoft AJAX о завершении загрузки скрипта. Обратите внимание, что это больше не требуется в .NET Framework 4 и выше.


Файл с именем GoogleMap.cs содержит весь код на стороне сервера. Открыв файл, первое, что вы заметите, это то, что он содержит класс GoogleMap , который наследуется от класса ScriptControl . ScriptControl — это абстрактный базовый класс, который наследуется от WebControl и реализует IScriptControl .

1
public class GoogleMap : ScriptControl

Хотя было бы хорошо оставить все как есть, мы можем создать более гибкую ситуацию, напрямую внедрив IScriptControl и вместо этого унаследовав от WebControl . Тем самым мы открываем возможность наследования от более сложного базового класса, такого как ListControl . Я также столкнулся с проблемами, унаследованными от ScriptControl при различных обстоятельствах. Давайте изменим это сейчас на следующее:

1
public class GoogleMap : WebControl, IScriptControl

GetScriptDescriptors конструктор, вы увидите методы GetScriptDescriptors и GetScriptReferences .

01
02
03
04
05
06
07
08
09
10
11
12
13
protected override IEnumerable<ScriptDescriptor>
            GetScriptDescriptors()
{
    ScriptControlDescriptor descriptor = new ScriptControlDescriptor(«MapControl.GoogleMap», this.ClientID);
    yield return descriptor;
}
 
// Generate the script reference
protected override IEnumerable<ScriptReference>
        GetScriptReferences()
{
    yield return new ScriptReference(«MapControl.GoogleMap.js», this.GetType().Assembly.FullName);
}

Поскольку мы реализуем IScriptControl напрямую, модификаторы уровня доступа необходимо будет изменить с защищенного на общедоступный, а модификаторы переопределения следует полностью удалить.

В методе ScriptControlDescriptor созданный и возвращенный объект ScriptControlDescriptor сообщает платформе, какой клиентский класс создать, а также передает идентификатор элемента HTML, связанного с текущим экземпляром элемента управления. Как вы увидите в следующем разделе, это также механизм, посредством которого значения свойств передаются из кода на стороне сервера в класс клиента.

Код в методе GetScriptReferences просто добавляет ScriptReference к нашему клиентскому файлу кода — он будет автоматически загружен платформой при необходимости.


Хорошо, теперь, когда у нас есть некоторая справочная информация, пришло время начать строить элемент управления карты. Для начала, мы добавим некоторые свойства к классу на стороне сервера (в GoogleMap.cs ), придерживаясь пока только GoogleMap.cs . Масштаб и центральная точка карты — это то, что приходит на ум как необходимые свойства.

  • Увеличить
  • CenterLatitude
  • CenterLongitude
1
2
3
4
5
6
7
8
9
private int _Zoom = 8;
public int Zoom
{
    get { return this._Zoom;
    set { this._Zoom = value;
}
 
public double CenterLatitude { get;
public double CenterLongitude { get;

Вы можете быть удивлены, как значения этих свойств, определенных в серверном классе, окажутся в клиентском классе. Ну, вот где ScriptControlDescriptor вступает в игру. Просто вызывая метод AddProperty объекта ScriptControlDescriptor и передавая имя и текущее значение свойства на стороне клиента, платформа заботится обо всех деталях.

1
2
3
4
5
6
7
8
public IEnumerable<ScriptDescriptor> GetScriptDescriptors()
{
    ScriptControlDescriptor descriptor = new ScriptControlDescriptor(«MapControl.GoogleMap», this.ClientID);
    descriptor.AddProperty(«zoom», this.Zoom);
    descriptor.AddProperty(«centerLatitude», this.CenterLatitude);
    descriptor.AddProperty(«centerLongitude», this.CenterLongitude);
    yield return descriptor;
}

Теперь нам нужно определить свойства в клиентском классе. Откройте GoogleMap.js и измените конструктор, чтобы он выглядел следующим образом:

1
2
3
4
5
6
7
MapControl.GoogleMap = function(element) {
    MapControl.GoogleMap.initializeBase(this, [element]);
 
    this._zoom = null;
    this._centerLatitude = null;
    this._centerLongitude = null;
}

Чтобы сделать эти свойства доступными для среды ASP.NET AJAX, нам нужно определить методы доступа get и set. Эти методы доступа должны следовать соглашениям об именах платформы — например, для свойства zoom get_zoom доступа должны называться get_zoom и set_zoom . Добавьте следующий код в объявление прототипа для класса:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
get_zoom: function() {
    return this._zoom;
},
set_zoom: function(value) {
    if (this._zoom !== value) {
        this._zoom = value;
        this.raisePropertyChanged(«zoom»);
    }
},
get_centerLatitude: function() {
    return this._centerLatitude;
},
set_centerLatitude: function(value) {
    if (this._centerLatitude !== value) {
        this._centerLatitude = value;
        this.raisePropertyChanged(«centerLatitude»);
    }
},
get_centerLongitude: function() {
    return this._centerLongitude;
},
set_centerLongitude: function(value) {
    if (this._centerLongitude !== value) {
        this._centerLongitude = value;
        this.raisePropertyChanged(«centerLongitude»);
    }
}

Метод raisePropertyChanged определен в классе-предке Sys.Component и вызывает событие propertyChanged для указанного свойства.


Мы напишем код, который создает карту всего за минуту, но сначала нам нужно определить свойство, которое будет хранить объект карты. Таким образом, мы сможем получить доступ к карте после ее создания — например, в обработчике событий. Добавьте следующее объявление свойства в конструктор для класса клиента ( GoogleMap.js ) после других свойств:

1
this._mapObj = null;

Теперь давайте добавим функцию createMap к прототипу:

1
2
3
4
5
6
7
8
9
createMap: function() {
        var centerPoint = new google.maps.LatLng(this.get_centerLatitude(), this.get_centerLongitude());
        var options = {
            zoom: this.get_zoom(),
            center: centerPoint,
            mapTypeId: google.maps.MapTypeId.ROADMAP
        };
      this._mapObj = new google.maps.Map(this._element, options);
}

Тип google.maps.LatLng определен в JavaScript API Google Карт (на который мы будем ссылаться позже) и, как вы уже догадались, представляет точку на карте, определяемую широтой / долготой. В параметрах карты мы устанавливаем масштаб и центральную точку карты на значения, передаваемые платформой. Тип карты дорожной карты установлен, но это может быть легко установлено на спутник или местность.

Последняя строка создает объект google.maps.Map , сохраняя ссылку на него в свойстве, которое мы создали выше.

Вы заметите, что конструктор принимает два параметра — прежде всего, первый — это ссылка на элемент HTML, связанный с элементом управления, — второй просто передает параметры карты.

Все, что осталось теперь на стороне клиента, — это вызвать нашу новую функцию createMap из функции initialize, чтобы карта создавалась при инициализации createMap управления.

1
2
3
4
5
initialize: function() {
    MapControl.GoogleMap.callBaseMethod(this, ‘initialize’);
 
    this.createMap();
},

Вернувшись к серверному коду ( GoogleMap.cs ), нам нужно переопределить свойство TagKey в нашем классе GoogleMap и вернуть значение HtmlTextWriterTag.Div . Это обеспечит отображение элемента управления в виде элемента html div.

1
2
3
4
5
6
7
protected override HtmlTextWriterTag TagKey
{
    get
    {
        return HtmlTextWriterTag.Div;
    }
}

Теперь давайте добавим приватное поле типа ScriptManager в класс — назовем его sm . Это сохранит ссылку на ScriptManager страницы, который мы будем использовать чуть позже.

1
private ScriptManager sm;

Далее мы переопределим методы OnPreRender и Render класса GoogleMap .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
protected override void OnPreRender(EventArgs e)
{
    if (!this.DesignMode)
    {
        // Test for ScriptManager and register if it exists
        sm = ScriptManager.GetCurrent(Page);
 
        if (sm == null)
            throw new HttpException(«A ScriptManager control must exist on the current page.»);
 
        sm.RegisterScriptControl(this);
    }
 
    base.OnPreRender(e);
}

Здесь мы в основном просто получаем ScriptManager текущей страницы (и проверяем, что он существует!), А затем регистрируем текущий экземпляр элемента управления с ним. Этот шаг, как и следующий, абсолютно необходим, иначе клиентская часть элемента управления не будет работать.

1
2
3
4
5
6
7
protected override void Render(HtmlTextWriter writer)
{
    if (!this.DesignMode)
        sm.RegisterScriptDescriptors(this);
 
    base.Render(writer);
}

Это регистрирует дескрипторы скрипта ScriptManager управления с помощью ScriptManager страницы — sm является ссылкой на ScriptManager который мы OnPreRender методе OnPreRender .

И наконец (пока!), Мы сделаем этот элемент управления совместимым со сценариями частичного доверия, что довольно часто встречается из-за популярности общего веб-хостинга. В обозревателе решений откройте папку «Свойства», а затем откройте AssemblyInfo.cs . Добавьте следующую ссылку в верхней части файла.

1
using System.Security;

Добавьте следующую строку где-то посередине или в нижней части файла.

1
[assembly: AllowPartiallyTrustedCallers()]

Теперь мы готовы взять контроль на тест-драйв. Мы вернемся позже и добавим больше функциональности, но сейчас давайте создадим небольшой веб-сайт для тестирования элемента управления. Щелкните правой кнопкой мыши узел решения в обозревателе решений и выберите Добавить -> Новый веб-сайт. Убедитесь, что веб-сайт ASP.NET выбран из списка. Назовите веб-сайт MapControlTest и нажмите ОК.

Во вновь созданном Default.aspx добавьте ScriptManager на страницу, прямо внутри элемента формы. Помните из последнего раздела, что это необходимо на странице, чтобы наш элемент управления GoogleMap работал.

1
2
3
4
5
6
7
8
<body>
    <form id=»form1″ runat=»server»>
    <asp:ScriptManager runat=»server» ID=»ScriptManager1″>
    </asp:ScriptManager>
    <div>
    </div>
    </form>
</body>

Щелкните правой кнопкой мыши MapControlTest веб-сайта MapControlTest в Solution Explorer и выберите Add Reference из всплывающего меню. Перейдите на вкладку Projects, выберите MapControl и нажмите OK. Теперь нам нужно зарегистрировать сборку и пространство имен в Default.aspx . Добавьте следующую строку в верхней части файла, чуть выше объявления DOCTYPE.

1
<%@ Register Namespace=»MapControl» TagPrefix=»mc» Assembly=»MapControl» %>

На этом этапе вы должны построить решение (Build -> Build Solution или клавишу F6), чтобы IntelliSense правильно работал для нового элемента управления. Теперь добавьте следующую декларацию управления на страницу.

1
2
<mc:GoogleMap runat=»server» CenterLatitude=»36.1658″ CenterLongitude=»-86.7844″ Width=»500″ Height=»500″>
</mc:GoogleMap>

Наконец, добавьте JavaScript API-интерфейс Google Maps (в верхней части страницы).

1
<script src=»http://maps.googleapis.com/maps/api/js?sensor=false»></script>

Теперь щелкните правой кнопкой мыши в редакторе (для Default.aspx ) и выберите «Просмотреть в браузере» во всплывающем меню. Ваш браузер должен открыть и загрузить страницу с картой в центре Нэшвилла, Теннесси. Вы можете поэкспериментировать со свойствами элемента управления, чтобы убедиться, что все работает правильно — в частности, попробуйте добавить свойство Zoom и посмотрите эффект на карте.


Но простая карта не так интересна, поэтому в этом разделе мы добавим некоторые функции, которые позволяют отображать маркеры на карте.

Сначала нам понадобится класс, который содержит информацию о каждом маркере. Щелкните правой кнопкой мыши MapControl проекта MapControl в обозревателе решений и выберите «Добавить» -> «Класс» во всплывающем меню. Назовите класс MapMarker и нажмите ОК.

Атрибут Serializable необходимо добавить в класс, чтобы он мог быть сериализован и передан в код на стороне клиента.

Добавьте свойства в класс, чтобы объявление класса выглядело так.

01
02
03
04
05
06
07
08
09
10
11
12
[Serializable]
public class MapMarker
{
    public double Latitude { get;
    public double Longitude { get;
    public string Title { get;
    public virtual string InfoWindowHtml { get;
 
    public MapMarker()
    {
    }
}

Свойства Latitude и Longitude довольно очевидны — свойство « Title предназначено для текста всплывающей подсказки, который будет отображаться при наведении InfoWindowHtml мыши на маркер, а свойство InfoWindowHtml будет содержать HTML- InfoWindowHtml отображаемый во всплывающем окне при щелчке маркера.

В серверном классе GoogleMap ( GoogleMap.cs ) мы добавим свойство списка MapMarkers . Атрибут PersistenceMode указывает, что свойство сохраняется в серверном элементе управления ASP.NET как вложенный тег.

1
2
3
4
5
6
7
private List<MapMarker> markers = new List<MapMarker>();
 
[PersistenceMode(PersistenceMode.InnerProperty)]
public List<MapMarker> Markers
{
    get { return this.markers;
}

ParseChildren также должен быть установлен в GoogleMap
класс для этого на работу.

1
2
[ParseChildren(true)]
public class GoogleMap : WebControl, IScriptControl

Теперь мы добавим другое свойство в ScriptControlDescriptor в методе GetScriptDescriptors для нового свойства.

1
2
3
4
5
6
7
8
9
public IEnumerable<ScriptDescriptor> GetScriptDescriptors()
{
    ScriptControlDescriptor descriptor = new ScriptControlDescriptor(«MapControl.GoogleMap», this.ClientID);
    descriptor.AddProperty(«zoom», this.Zoom);
    descriptor.AddProperty(«centerLatitude», this.CenterLatitude);
    descriptor.AddProperty(«centerLongitude», this.CenterLongitude);
    descriptor.AddProperty(«markers», this.Markers);
    yield return descriptor;
}

Возвращаясь к клиентскому коду ( GoogleMap.js ), добавьте новый
объявление свойства с именем _markers для конструктора. Пока мы
здесь, давайте также добавим свойство с именем _infoWindow , которое будет
сохранить ссылку на всплывающее окно, которое появляется при нажатии на маркер.

01
02
03
04
05
06
07
08
09
10
11
MapControl.GoogleMap = function(element) {
    MapControl.GoogleMap.initializeBase(this, [element]);
 
    this._zoom = null;
    this._centerLatitude = null;
    this._centerLongitude = null;
    this._markers = null;
 
    this._mapObj = null;
    this._infoWindow = null;
}

Теперь нам нужно добавить методы доступа get и set для свойства, как и раньше.

1
2
3
4
5
6
7
8
9
get_markers: function() {
    return this._markers;
},
set_markers: function(value) {
    if (this._markers !== value) {
        this._markers = value;
        this.raisePropertyChanged(«markers»);
    }
},

Теперь осталось только добавить код, который будет отображать маркеры для функции createMap .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
createMap: function() {
    var centerPoint = new google.maps.LatLng(this.get_centerLatitude(), this.get_centerLongitude());
    var options = {
        zoom: this.get_zoom(),
        center: centerPoint,
        mapTypeId: google.maps.MapTypeId.ROADMAP
    };
 
    this._mapObj = new google.maps.Map(this._element, options);
 
    var markers = this.get_markers();
    if (markers != null) {
        for (var i = 0; i < markers.length; i++) {
            var marker = new google.maps.Marker
            (
                {
                    position: new google.maps.LatLng(markers[i].Latitude, markers[i].Longitude),
                    map: this._mapObj,
                    title: markers[i].Title
                }
            );
 
            var that = this;
            (function(marker, infoHtml) {
                google.maps.event.addListener(marker, ‘click’, function() {
 
                    if (!that._infoWindow) {
                        that._infoWindow = new google.maps.InfoWindow();
                    }
 
                    that._infoWindow.setContent(infoHtml);
 
                    that._infoWindow.open(that._mapObj, marker);
 
                });
            })(marker, markers[i].InfoWindowHtml);
        }
    }
}

Это может показаться сложным, но на самом деле это не так — я разобью его на части и рассмотрю по частям.

1
2
3
var markers = this.get_markers();
if (markers != null) {
   for (var i = 0; i < markers.length; i++) {

Здесь мы просто получаем массив маркеров, проверяем, что это не пустая ссылка, и перебираем массив.

1
2
3
4
5
6
7
8
var marker = new google.maps.Marker
(
    {
        position: new google.maps.LatLng(markers[i].Latitude, markers[i].Longitude),
        map: this._mapObj,
        title: markers[i].Title
    }
);

Этот фрагмент кода — это то, что фактически добавляет маркер на карту — захват position и title маркера из массива с помощью markers[i] и установка свойства map для карты, созданной ранее.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
var that = this;
(function(marker, infoHtml) {
    google.maps.event.addListener(marker, ‘click’, function() {
 
        if (!that._infoWindow) {
            that._infoWindow = new google.maps.InfoWindow();
        }
 
        that._infoWindow.setContent(infoHtml);
 
        that._infoWindow.open(that._mapObj, marker);
 
    });
})(marker, markers[i].InfoWindowHtml);

Первая строка — сохранить текущий контекст в обработчике событий — он больше не будет ссылаться на наш пользовательский элемент управления, поэтому мы используем его вместо этого. Поскольку мы выполняем итерацию по массиву, а переменные marker и i меняются, нам нужно передать переменные marker и infoHtml в новую самозапускающуюся анонимную функцию, чтобы при запуске обработчика события (который будет позже), переменные по-прежнему имеют свои правильные значения. Код внутри обработчика событий довольно прост — в основном это просто установка содержимого и открытие всплывающего информационного окна.

Теперь мы можем добавить маркеры на карту следующим образом (вам может понадобиться сначала создать решение, чтобы IntelliSense работал).

1
2
3
4
5
<mc:GoogleMap runat=»server» CenterLatitude=»36.1658″ CenterLongitude=»-86.7844″ Width=»500″ Height=»500″>
    <Markers>
        <mc:MapMarker Latitude=»36.1658″ Longitude=»-86.7844″ Title=»Nashville, TN» InfoWindowHtml=»<strong>Nashville, TN</strong>» />
    </Markers>
</mc:GoogleMap>

Если маркеры будут размещены на карте через пользовательский ввод, возникнут проблемы с безопасностью — в частности, вам необходимо убедиться, что пользовательский ввод для свойства Title закодирован в формате html, а входные данные для свойства InfoWindowHtml не содержат каких-либо опасных код. Как всегда, безопасность в производственном приложении должна быть главной задачей.


Надеемся, что в этом руководстве вы узнали не только о создании настраиваемого серверного серверного элемента управления ASP.NET AJAX с функциональностью на стороне клиента, но и немного об API Карт Google. Некоторые из вас могут подумать, что эта технология устарела, поскольку она применяется к веб-формам ASP.NET. Тем не менее, веб-формы все еще являются очень жизнеспособным вариантом для новой веб-разработки, особенно для отдельных разработчиков, работающих над короткими проектами и нуждающихся в быстром выполнении задач.

ASP.NET MVC, вероятно, лучше с точки зрения гибкости и тестируемости. Дело в том, что это руководство не защищает использование одной технологии над другой; он просто учит технике, которая может быть очень полезна при создании приложений ASP.NET Web Forms.