Руководство разработчика Prism — часть 9, взаимодействие между слабо связанными компонентами

в 11:11, , рубрики: .net, communication, components, expression blend, loose coupling, microsoft, mvvm, patterns and practices, Prism, silverlight, Visual Studio, wpf, интерфейсы

Оглавление

  1. Введение
  2. Инициализация приложений Prism
  3. Управление зависимостями между компонентами
  4. Разработка модульных приложений
  5. Реализация паттерна MVVM
  6. Продвинутые сценарии MVVM
  7. Создание пользовательского интерфейса
    1. Рекомендации по разработке пользовательского интерфейса
  8. Навигация
    1. Навигация на основе представлений (View-Based Navigation)
  9. Взаимодействие между слабо связанными компонентами

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

При построении модели взаимодействия между модулями, необходимо знать отличия между подходами, чтобы знать, какой из них применить в конкретном сценарии. Библиотека Prism предоставляет следующие подходы:

  • Использование команд (Solution commanding). Используйте для реагирования на действия пользователя.
  • Контекст региона (Region context). Используйте для передачи контекстной информации от host-элемента управления к представлениями в регионе. Этот подход в некотором роде аналогичен DataContext, но не полагается на него.
  • Общие службы (Shared services). Вы можете вызвать метод на сервисе, который, в свою очередь, сгенерирует событие, на которое могут быть подписаны получатели. Используйте этот подход в том случае, если все остальные подходы не применимы.
  • Агрегация событий (Event aggregation). Для передачи сообщений между моделями представлений, презентерами, или контроллерами при отсутствии ожиданий о непосредственной реакции на сообщение.

Использование команд (Solution commanding)

Если вам нужно реагировать на жесты пользователя, такие как нажатие кнопки, и если вы хотите управлять доступностью элемента управления в зависимости от логики приложения, используйте команды.

WPF предоставляет маршрутизируемые команды (RoutedCommand), которые хорошо подходят для связи элемента управления, вызывавшего команду, такого как кнопка, или элемент меню, с обработчиком этой команды, ассоциированным с текущим элементом, имеющим фокус клавиатуры.

Однако в сценариях составного приложения, обработчиком события часто является модель представления, или не имеющая каких-либо ассоциированных элементов в визуальном дереве, или которые не имеют фокуса. Для поддержки таких сценариев, Prism предоставляет класс DelegateCommand, который позволяет вызывать делегат при выполнении команды, и класс CompositeCommand, позволяющий комбинировать несколько команд в одну. Эти классы отличаются от встроенного класса RoutedCommand, маршрутизирующего обработку команды вниз и вверх по визуальному дереву. Они позволяют инициировать команду в визуальном дереве, а обработать её на более высоком уровне.

Класс CompositeCommand реализует интерфейс ICommand, поэтому он может быть привязан к элементам управления. Класс CompositeCommands может быть ассоциирован с несколькими дочерними командами. При выполнении CompositeCommand, дочернии команды также будут выполнены.

Класс CompositeCommand поддерживает активизацию. При возникновении события CanExecuteChanged, он сам генерирует это событие. После этого, ассоциированный элемент управления вызывает метод CanExecute на CompositeCommand. CompositeCommand опрашивает все дочернии команды, вызывая их метод CanExecute. Если какая-либо команда возвращает false, CompositeCommand тоже возвращает false, деактивируя, таким образом, элемент управления.

Приложения, основывающиеся на библиотеке Prism, могут иметь глобальные команды CompositeCommand, определённые в оболочке, имеющие смысл по всему приложению, такие как Save, Save All и Cancel. Модули могут регистрировать свои локальные команды в этих глобальных командах принимая, таким образом, участие в их выполнении.

Заметка про WPF Routed Events (маршрутизируемые события) и Routed Commands (маршрутизируемые команды).
Маршрутизируемые события отличаются от обычных событий тем, что обрабатывать их могут несколько обработчиков, расположенных в различных местах дерева элементов. Маршрутизируемые события WPF переносят сообщения между элементами визуального дерева. Элементы не находящиеся в визуальном дереве, не могут обрабатывать эти команды. Маршрутизируемые события могут использоваться для коммуникации между элементами визуального дерева, так как данные о событии сохраняются для каждого элемента в маршруте. Один элемент может изменить что-то в этих данных, и это изменение будет доступно для следующего элемента в маршруте.

Вы можете использовать маршрутизируемые события WPF в следующих случаях: задание общего обработчика в общем корневом элементе, или при создании собственного класса элемента управления.

Создание команды-делегата (Delegate command)

Для создания команды-делегата необходимо создать и инициализировать поле типа DelegateCommand в конструкторе модели представления и сделать его доступным через свойство типа ICommand.

public class ArticleViewModel : NotificationObject {
    private readonly ICommand _showArticleListCommand;

    public ArticleViewModel(INewsFeedService newsFeedService,
                            IRegionManager regionManager,
                            IEventAggregator eventAggregator) {
        _showArticleListCommand = new DelegateCommand(this.ShowArticleList);
    }

    public ICommand _showArticleListCommand { get { return _showArticleListCommand; } }
}

Заметка.
Для редко используемых команд часто имеет смысл «лениво» создавать DelegateCommand непосредственно в методе get свойства.

public class ArticleViewModel : NotificationObject {
    private readonly ICommand _showArticleListCommand;

    public ArticleViewModel(INewsFeedService newsFeedService,
                            IRegionManager regionManager,
                            IEventAggregator eventAggregator) { }

    public ICommand _showArticleListCommand {
        get {         
            return _showArticleListCommand ??
                ( _showArticleListCommand = new DelegateCommand(this.ShowArticleList) ); 
        } 
    }
}

Создание составной команды (Composite command)

Для создания составной команды необходимо создать и инициализировать поле типа CompositeCommand в конструкторе модели представления и сделать его доступным через свойство типа ICommand.

public class MyViewModel : NotificationObject {
    private readonly CompositeCommand _saveAllCommand;

    public ArticleViewModel(INewsFeedService newsFeedService,
                            IRegionManager regionManager,
                            IEventAggregator eventAggregator) {
        _saveAllCommand = new CompositeCommand();
        _saveAllCommand.RegisterCommand(new SaveProductsCommand());
        _saveAllCommand.RegisterCommand(new SaveOrdersCommand());
    }

    public ICommand _saveAllCommand { get { return _saveAllCommand; } }
}

Создание глобально доступных команд

Обычно, для создания глобально доступной команды, создаётся экземпляр DelegateCommand, или CompositeCommand и делается доступным через статический класс.

public static class GlobalCommands {
    public static CompositeCommand MyCompositeCommand = new CompositeCommand();
}

Затем, в модуле производится ассоциция дочерних команд с глобальной командой.

GlobalCommands.MyCompositeCommand.RegisterCommand(command1);
GlobalCommands.MyCompositeCommand.RegisterCommand(command2);

Заметка.
Для улучшения тестируемости кода, вы можете использовать класс-посредник для доступа к глобально доступной команде и мокировать его в тестах.

Привязка к глобально доступным командам

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

<Button Name="MyCompositeCommandButton" 
        Command="{x:Static local:GlobalCommands.MyCompositeCommand}"
        Content="Execute My Composite Command">    
</Button>

Silverlight не предоставляет поддержку для x:static, поэтому для привязки кнопки в Silverlight, необходимо выполнить следующие шаги:

  1. В модели представления необходимо создать public свойство для получения команды, заданной в статическом классе.
    public ICommand MyCompositeCommand { get { return GlobalCommands.MyCompositeCommand; } }
    

  2. Обычно, модели представления ассоциируются с представлениями через DataContext (что делается в файле отделённого кода).
    view.DataContext = model;
    

  3. Убедитесь, что в корневой элемент добавлено следующее пространство имён XML.
    xmlns:prism="clr-namespace:Microsoft.Practices.Prism.Commands;assembly=Microsoft.Practices.Prism"
    

  4. Привяжите кнопку к команде, используя присоединённое свойство Click.Command.
    <Button Name="MyCommandButton" 
            prism:Click.Command="{Binding MyCompositeCommand}"
            Content="Execute MyCommand"/>    
    </Button>
    

Заметка.
Другим подходом является сохранение команды как ресурс в файле App.xaml в разделе Application.Resources. После этого в представлении сделать к нему привязку.

<Button Name="MyCommandButton" 
        prism:Click.Command="{Binding MyCompositeCommand, Source={StaticResource GlobalCommands}}
        Content="Execute MyCommand"/>    
</Button>

Контекст региона (Region context)

Существует множество сценариев, когда может понадобиться передавать контекстную информацию между представлением, ассоциированным с регионом (хостом), и представлениями, находящимися в этом регионе. Для примера, в сценарии master-detail представление отображает сущность и регион, в котором выводится дополнительная информация по этой сущности. Prism использует концепт под названием RegionContext для передачи объекта между хостом региона и представлениями, загруженными в этот регион, как показано на иллюстрации ниже.

Использование RegionContext

В зависимости от сценария, вы можете предать как часть информации (такую, как идентификатор), так и всю модель. Представление может получить RegionContext и затем ждать уведомления об изменении. Представление также может изменить значение RegionContext. Существует несколько способов получения и задания с RegionContext:

  • Задание RegionContext региона, используя XAML.
  • Задание RegionContext региона в коде.
  • Получение RegionContext от представления, находящегося внутри региона.

Заметка.
В настоящий момент, Prism поддерживает получение RegionContext от представления внутри региона, если это представление является наследником DependencyObject. Если ваше представление не является DependencyObject (к примеру, если вы используете шаблоны данных и добавляете модель представления непосредственно в регион), рассмотрите создание собственного класса RegionBehavior для передачи RegionContext в объекты представления.

Заметка про свойство DataContext.
Контекст данных является концептом, который позволяет элементам управления наследовать информацию об источнике данных для привязки от их родительских элементов. Дочерние элементы автоматически наследуют DataContext от их родительских элементов. Данные распространяются вниз по визуальному дереву.

Лучшим путём для привязки модели представления к представлению в Silverlight, является использование свойства DataContext. Именно поэтому, в большинстве сценариев, свойство DataContext используется для хранения модели представления. Именно из-за этого, не рекомендуется использовать DataContext в качестве коммуникационного механизма между слабо связанными представлениями.

Общие службы (Shared services)

Ещё одним способом осуществления межмодульного взаимодействия, является использование общих служб. При загрузке модуля, он может добавлять свои службы в DI контейнер. Обычно, службы регистрируются и запрашиваются из контейнера по общим интерфейсам, которые располагаются в отдельной сборке. Это позволяет модулям использовать службы, предоставляемые другими модулями, без необходимости создания статических ссылок на эти модули. Экземпляры служб являются общими для всех модулей, так что вы можете обмениваться данными и посылать сообщения между ними.

В Stock Trader RI, модуль Market предоставляет реализацию IMarketFeedService. Модуль Position потребляет эту службу, используя DI контейнер, определённый в оболочке приложения. IMarketFeedService предназначен для использования его другими модулями, поэтому он расположен в общей сборке StockTraderRI.Infrastructure, но его конкретная реализация не должна быть общедоступной, поэтому она определена в модуле Market и может меняться независимо от других модулей.

Для того, чтобы увидеть, как эти службы экспортируются в контейнер MEF, смотрите файлы MarketFeedService.cs и MarketHistoryService.cs. Класс ObservablePosition модуля Position получает службу IMarketFeedService через внедрение зависимости в конструктор.

[Export(typeof(IMarketFeedService))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class MarketFeedService : IMarketFeedService, IDisposable {
    ...
}

Заметка.
Некоторые DI контейнеры позволяют регистрировать зависимости с использованием атрибутов, как было показано на примере выше. Другие могут потребовать явной регистрации. В таких случаях, регистрация обычно осуществляется во время загрузки модуля в методе IModule.Initialize. Для получения дополнительной информации, смотрите часть 4 "Разработка модульных приложений".

Агрегация событий (Event aggregation)

Библиотека Prism предоставляет механизм, позволяющий осуществлять взаимодействие между слабо связанными компонентами в приложении. Этот механизм основан на службе агрегации событий и шаблоне Publisher-Subscriber, позволяющей издателям (publisher) и подписчикам (subscriber) взаимодействовать, не имея явных ссылок друг на друга.

EventAggregator предоставляет функциональность мультиадресной публикации/подписки. Это означает, что может существовать несколько издателей, публикующих событие, и несколько подписчиков, подписанных на это событие. Рассмотрите использование EventAggregator для публикации событий между несколькими модулями и при посылке сообщений между кодом бизнес логики, таким как контроллеры и презентеры.

Примером этому может служить Stock Trader RI, в котором при нажатии кнопки Process Order запускается обработка заказа. В этом случае, несколько модулей должны знать, что заказ обработан, чтобы обновить свои представления.

События, созданные с помощью Prism, являются строго типизированными. Это означает, что вы можете воспользоваться преимуществом проверки типов во время компиляции. Класс EventAggregator позволяет подписчикам, или издателям обнаруживать конкретного наследника EventBase для определения типа события. Им могут пользоваться сразу несколько подписчиков и издателей, как показано на иллюстрации ниже.

Агрегация событий

Заметка о событиях в .NET Framework.
Использование встроенного в .NET Framework механизма событий является наиболее простым и очевидным способом взаимодействия компонентов друг с другом, если не требуется слабая связанность. События в .NET Framework реализуют шаблон Publish-Subscribe, но для подписки на событие объекта, требуется непосредственная ссылка на этот объект, который, в составных приложениях, зачастую находится в другом модуле. Результатом этому является сильно связанный дизайн. Следовательно, события .NET Framework должны использоваться для обмена сообщениями внутри модулей, но не между ними.

Если вы используете события .NET Framework, вы должны быть очень осторожны, чтобы не допустить утечек памяти, особенно, если у вас есть нестатические, или короткоживущие компоненты, которые подписываются на события статического или долгоживущего объекта. Если подписчик не отпишется от подписки, ссылка на него будет сохранена издателем, что будет препятствовать освобождению первого сборщиком мусора.

Интерфейс IEventAggregator

Получение службы EventAggregator из контейнера происходит через интерфейс IEventAggregator. Агрегатор событий ответственен за обнаружение, или создание и сохранение объектов событий.

public interface IEventAggregator {        
    TEventType GetEvent<TEventType>() where TEventType : EventBase;
}

EventAggregator создаёт объект события при первом запросе, если он ещё не был создан. Он освобождает издателя, или подписчика от необходимости определять, доступно ли событие.

Класс CompositePresentationEvent

Реальная работа по соединению издателей с подписчиками совершается классом CompositePresentationEvent. Он является единственной реализацией класса EventBase, включенной в библиотеку Prism. Он поддерживает список подписчиков и обрабатывает посылку события этим подписчикам.

Класс CompositePresentationEvent является обобщённым и требует определения типа полезной нагрузки. Это помогает издателям и подписчикам предоставлять методы с корректной сигнатурой для успешного подключения событий. Следующий код показывает частичное определение класса CompositePresentationEvent.

public class CompositePresentationEvent<TPayload> : EventBase {
    ...
    public SubscriptionToken Subscribe(Action<TPayload> action);
    public SubscriptionToken Subscribe(Action<TPayload> action, ThreadOption threadOption);
    public SubscriptionToken Subscribe(Action<TPayload> action, bool keepSubscriberReferenceAlive)
    public virtual SubscriptionToken Subscribe(Action<TPayload> action, ThreadOption threadOption, bool keepSubscriberReferenceAlive); 
    public virtual SubscriptionToken Subscribe(Action<TPayload> action, ThreadOption threadOption, bool keepSubscriberReferenceAlive, Predicate<TPayload> filter);
    public virtual void Publish(TPayload payload);
    public virtual void Unsubscribe(Action<TPayload> subscriber); 
    public virtual bool Contains(Action<TPayload> subscriber)
    ...
}

Создание и публикация событий

В следующих разделах описывается, как создавать, публиковать и подписываться на события CompositePresentationEvent, используя интерфейс IEventAggregator.

Создание события

Класс CompositePresentationEvent<TPayload> предназначен служить базовым классом для событий, характерных для модуля, или приложения. TPayLoad является типом полезной нагрузки события. Полезная нагрузка является аргументом, который будет передан подписчику при публикации события.

Для примера, следующий код показывает класс TickerSymbolSelectedEvent в Stock Trader RI. Полезной нагрузкой является строка, содержащая символ компании. Обратите внимание, что тело класса пусто.

public class TickerSymbolSelectedEvent : CompositePresentationEvent<string> { }

Заметка.
В составных приложениях, события зачастую доступны нескольким модулям, поэтому их необходимо помещать в общедоступном месте. В Stock Trader RI, все они размещены в проекте StockTraderRI.Infrastructure.

Публикация события

Издатели публикуют события с помощью получения объекта события через EventAggregator и вызова метода Publish. Для получения EventAggregator, вы можете использовать внедрение зависимости, добавив параметр типа IEventAggregator в конструктор класса.

Для примера, следующий код показывает публикацию события TickerSymbolSelectedEvent.

this.eventAggregator
    .GetEvent<TickerSymbolSelectedEvent>()
    .Publish("STOCK0");

Подписка на события

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

  • Если вам нужно обновлять элементы управления в пользовательском интерфейсе при получении события, подписывайтесь на получение события в потоке UI.
  • Если вам нужно фильтровать события, предоставьте при подписке делегат для фильтрации.
  • Если вам критична производительность, рассмотрите использование сильных ссылок при подписке и последующее ручную отписку.
  • Если ни один из пунктов выше вам не подходит, используйте стандартную подписку.

В следующих разделах эти варианты будут рассмотрены подробнее.

Подписка в потоке UI

Довольно часто, подписчикам будет необходимо обновлять элементы UI в ответ на событие. В WPF и Silverlight, только поток UI может обновлять элементы UI.

По умолчанию, подписчик получает событие в потоке издателя. Если издатель посылает событие из потока UI, подписчик может свободно обновлять UI. Однако если поток издатель является фоновым, подписчик не сможет напрямую обновлять элементы UI. В этом случае, подписчику необходимо будет запланировать обновление UI, используя класс Dispatcher.

CompositePresentationEvent может помочь в данной ситуации, позволяя подписчику автоматически получать события в потоке UI. Подписчик обозначает это во время подписки, как показано в коде ниже.

public void Run() {
    ...
    this.eventAggregator
       .GetEvent<TickerSymbolSelectedEvent>()
       .Subscribe(ShowNews, ThreadOption.UIThread);
}

public void ShowNews(string companySymbol) {
    this.articlePresentationModel.SetTickerSymbol(companySymbol);
}

Перечисление ThreadOption содержит следующие члены:

  • PublisherThread. События будут получены в потоке издателя. Это является стандартным поведением.
  • BackgroundThread. Событие будет получено асинхронно в потоке из пула потоков .NET Framework.
  • UIThread. Событие будет получено в потоке UI.
Фильтрация при подписке

Подписчики могут не желать обрабатывать каждый экземпляр опубликованного события. В таких случаях, подписчик должен использовать параметр filter. Этот параметр имеет тип System.Predicate<TPayLoad> и позволяет задать делегат, который будет вызываться при публикации события, для определения, следует ли реагировать на это событие.

Зачастую, фильтр представляет собой лямбда-выражение, как показано ниже.

FundAddedEvent fundAddedEvent = this.eventAggregator.GetEvent<FundAddedEvent>();

fundAddedEvent.Subscribe(
    FundAddedEventHandler,
    ThreadOption.UIThread,
    false,
    fundOrder => fundOrder.CustomerId == this.customerId);

Заметка.
Из-за модели безопасности Silverlight, метод фильтра должен быть публично доступным, если вы хотите использовать слабые ссылки при подписке (что является поведением по умолчанию для CompositePresentationEvent). Так как лямбда-выражения и анонимные делегаты генерируются как приватные классы и методы, они не могут использоваться вместе со слабыми ссылками в Silverlight. Вместо этого, вы должны использовать или public методы, или установить параметр keepSubscriberReferenceAlive в true, чтобы заставить использовать сильные ссылки при подписке (смотрите пример ниже).

Из-за модели безопасности Silverlight, вы должны будете вызывать отдельный public метод, как показано на примере ниже, или подписываться, используя сильные ссылки (будет показано далее в примерах).

public bool FundOrderFilter(FundOrder fundOrder){
    return fundOrder.CustomerId == this.customerId;
}
...

FundAddedEvent fundAddedEvent = this.eventAggregator.GetEvent<FundAddedEvent>();

subscriptionToken = fundAddedEvent.Subscribe(
    FundAddedEventHandler, 
    ThreadOption.UIThread, 
    false, 
    FundOrderFilter);

Заметка.
Метод Subscribe возвращает токен подписки типа Microsoft.Practices.Prism.Events.SubscriptionToken, который может использоваться для удаления подписки на событие. Этот токен особенно полезен при использовании анонимных делегатов, или лямбда-выражений, или когда вы используете один и тот же обработчик, но с разными фильтрами.

Заметка.
Не рекомендуется изменять объект полезной нагрузки в методе обратного вызова, так как несколько потоков могут одновременно запрашивать этот объект. Лучше всего делать объект полезной нагрузки неизменяемым для предотвращения возможных ошибок.

Подписка с использованием сильных ссылок (Strong references)

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

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

Для создания подписки с использованием сильной ссылки, используйте параметр keepSubscriberReferenceAlive метода Subscribe, как показано на примере ниже.

FundAddedEvent fundAddedEvent = eventAggregator.GetEvent<FundAddedEvent>();

bool keepSubscriberReferenceAlive = true;

fundAddedEvent.Subscribe(
    FundAddedEventHandler, 
    ThreadOption.UIThread, 
    keepSubscriberReferenceAlive, 
    fundOrder => fundOrder.CustomerId == _customerId);

Параметр keepSubscriberReferenceAlive типа bool:

  • При установке в true, экземпляр события хранит сильную ссылку на экземпляр подписчика, не позволяя ему тем самым стать достижимым для сборщика мусора. Про отписку от события, смотрите раздел далее с статье.
  • При установке в false (что является значением по умолчанию, если не задать этот параметр), экземпляр события создаёт слабую ссылку на подписчика, позволяя ему стать достижимым для сборщика мусора при отсутствии на него других сильных ссылок. При сборке мусора, происходит автоматическая отписка от этого события.
Стандартная подписка

Для создания подписки, от подписчика необходим, как минимум, делегат обратного вызова с подходящей сигнатурой для получения уведомления о событии. Для примера, обработчик для TickerSymbolSelectedEvent требует метода, принимающего string параметр, как показано ниже.

public void Run() {
    ...
    this.eventAggregator
        .GetEvent<TickerSymbolSelectedEvent>()
        .Subscribe(ShowNews);
}

public void ShowNews(string companySymbol) {
    articlePresentationModel.SetTickerSymbol(companySymbol);
}
Отказ от подписки

Если подписчик более не желает получать уведомления, то он может отписаться, используя или тот же самый обработчик, или токен подписки.

Следующий пример кода показывает, как отписаться, используя обработчик.

FundAddedEvent fundAddedEvent = this.eventAggregator.GetEvent<FundAddedEvent>();

fundAddedEvent.Subscribe(FundAddedEventHandler, ThreadOption.PublisherThread);

fundAddedEvent.Unsubscribe(FundAddedEventHandler);

Следующий код показывает, как отписаться, используя токен подписки. Токен возвращается методом Subscribe при создании подписки.

FundAddedEvent fundAddedEvent = this.eventAggregator.GetEvent<FundAddedEvent>();

subscriptionToken = fundAddedEvent.Subscribe(
    FundAddedEventHandler, 
    ThreadOption.UIThread, 
    false, 
    fundOrder => fundOrder.CustomerId == this.customerId);

fundAddedEvent.Unsubscribe(subscriptionToken);

Дополнительная информация

Для получения дополнительной информации о слабых ссылках, смотрите статью «Weak References» на MSDN: http://msdn.microsoft.com/en-us/library/ms404247.aspx.

Автор: Unrul

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js