protected override void OnNavigatedTo(NavigationEventArgs e) { DataTransferManager.GetForCurrentView().DataRequested += Share_DataRequested; } protected override void OnNavigatingFrom (NavigatingCancelEventArgs e) { DataTransferManager.GetForCurrentView().DataRequested -= Share_DataRequested; }
void Share_DataRequested(DataTransferManager sender, DataRequestedEventArgs args) { args.Request.Data.Properties.Title = "Simple notepad"; args.Request.Data.Properties.Description = "Publication of the text"; args.Request.Data.SetText("Hello! World!"); }
protected override void OnShareTargetActivated (ShareTargetActivatedEventArgs args) { var frame = new Frame(); frame.Navigate(typeof(TargetPage), args.ShareOperation); Window.Current.Content = frame; Window.Current.Activate(); }
protected async override void OnNavigatedTo(NavigationEventArgs e) { if (e.Parameter != null) { var shareOperation = (ShareOperation)e.Parameter; txtBlock.Text=await shareOperation.Data.GetTextAsync(); } }
args.Request.Data.SetText("..."); args.Request.Data.SetRtf("..."); args.Request.Data.SetHtmlFormat("...");
shareOperation.Data.GetTextAsync(); shareOperation.Data.GetRtfAsync(); shareOperation.Data.GetHtmlFormatAsync();
if(shareOperation.Data.Contains(StandardDataFormats.Text)) { var text = shareOperation.Data.GetTextAsync(); } if(shareOperation.Data.Contains(StandardDataFormats.Html)) { var html = shareOperation.Data.GetHtmlFormatAsync(); } if(shareOperation.Data.Contains(StandardDataFormats.Rtf)) { var rtf = shareOperation.Data.GetRtfAsync(); }
args.Request.Data.SetUri(new Uri ("http://akhmed.ru/post/2012/07/19/share_contract_win8.aspx"));
args.Request.Data.SetBitmap(RandomAccessStreamReference. CreateFromUri(new Uri("http://freshyourmood.com/wp-content/ uploads/2012/04/pure-water-aquatecuk.wordpress.com_.jpg")));
args.Request.Data.SetBitmap(RandomAccessStreamReference. CreateFromUri(new Uri("ms-appx:///apple.jpg")));
<StackPanel Orientation="Horizontal" VerticalAlignment="Top"> <Image x:Name="img" Width="200" Height="200"></Image> <TextBlock x:Name="txtBlock" Margin="24" Text="start page text" Style="{StaticResource HeaderTextStyle}" /> </StackPanel>
protected async override void OnNavigatedTo (NavigationEventArgs e) { if (e.Parameter != null) { var shareOperation = (ShareOperation) e.Parameter; if (shareOperation.Data.Contains (StandardDataFormats.Text)) { txtBlock.Text = await shareOperation.Data.GetTextAsync(); } if (shareOperation.Data.Contains (StandardDataFormats.Bitmap)) { var bitmapReference = await shareOperation.Data.GetBitmapAsync(); var bitmapImage = new BitmapImage(); bitmapImage.SetSource (await bitmapReference.OpenReadAsync()); img.Source = bitmapImage; } } }
args.Request.Data.Properties.Thumbnail = RandomAccessStreamReference.CreateFromUri(new Uri ("ms-appx:///apple.jpg"));
var bitmapReference = shareOperation. Data.Properties.Thumbnail;
Таким образом мы можем передавать сразу две картинки но из соображений производительности использовать второй подход стоит только в том случае если картинки расположены локально и они не большого размера.
Отправка и прием файлов
Через контракты мы можем передавать файлы доступные в системе. По умолчанию в WinRT у нас нет прямого доступа к файловой системе и мы можем передавать файлы, полученные, к примеру, через контракт FilePicker. В целом работа с файлами заслуживает отдельной статьи, но пользоваться контрактом FilePicker очень просто.
К примеру во время работы с приложением мы можем загрузить несколько файлов в наше приложение.
Для простоты добавим кнопку по клику на которую выбираем файлы:
<Button Content="LoadFiles" Click="LoadFiles_Click" HorizontalAlignment="Left" Margin="268,82,0,0" VerticalAlignment="Top" Width="139"/>
И сохраняем ссылку на выбранные пользователем файлы:
private IReadOnlyList<StorageFile> files; private async void LoadFiles_Click (object sender, RoutedEventArgs e) { var filePicker = new FileOpenPicker { FileTypeFilter = {"*"} }; files = await filePicker.PickMultipleFilesAsync(); }
Теперь, на событие вызова шаринга мы укажем что хотим передать эти файлы:
void Share_DataRequested(DataTransferManager sender, DataRequestedEventArgs args) { args.Request.Data.Properties.Title = "file sharing"; if(files!=null) { args.Request.Data.SetStorageItems(files); } }
Реализация «приемника» файлов тоже достаточно простая. В манифесте мы должны добавить поддержку файлов StorageItems или поставить галочку на Supports any file types
Далее, для примера, мы выведем список файлов в ListBox:
<ListBox x:Name="listBox"></ListBox>
И привяжем к этому списку имена полученных файлов
protected async override void OnNavigatedTo (NavigationEventArgs e) { if (e.Parameter != null) { var shareOperation = (ShareOperation)e.Parameter; var files=await shareOperation.Data.GetStorageItemsAsync(); listBox.ItemsSource = files.Select(i=>i.Name); } }
В итоге мы можем увидеть примерно следующий результат:
Отправка и прием нестандартных типов данных.
Наличие стандартных форматов значительно упрощает работу для большинства сценариев. Но все же довольно часто будут нужны сценарии когда нам не подходит ни один из форматов. Допустим у нас есть приложение которое работает со списком продуктов. У нас есть множество полей часть которых являются опциональными (Название, цена, категория, бренд, единица измерения и т.п.). Теперь если у нас есть другое приложение которое может тоже работать, с этим форматом продуктов и мы захотим добавить возможность поставить данные в том виде какие они у нас есть другому приложению.
Отправка и прием объектов с сериализацией в строку.
Мы можем сериализовать наши данные в какой нибудь формат (CSV, XML, JSON) и распарсить его для получения. Но нам не подойдут стандартные форматы. Во первых если мы используем для этого Text или другой формат то откроются все приложения которые поддерживают стандартный формат и выбрав приложение почты пользователь увидит письмо наполненное каким то непонятным для него текстом. Кроме того мы все же хотим сохранить возможность использовать формат Text или HTML для того, что бы была возможность отправить данные другому пользователю список продуктов без деталей.
В этом случае нам может помочь специальный формат данных.
В первую очередь нам необходимо придумать название формата, например Product. (Возможно до или после нас кому то придет в голову гениальная идея использовать формат с таким же названием и тоже назовет свой формат Product, что бы избежать конфликтов можно использовать Product_{GUID} (Например Product_F6FBDBFB-5703-44F6-ACBE-A2D25EF4D6CE). Здесь же для простоты оставим Product).
Допустим и в приемнике и в потавщике у нас есть следующий класс Product:
public class Product { public string Name { get; set; } public int Count { get; set; } }
public class JsonSerializer { public static string Serialize<T>(T item) { var dataSerializer = new DataContractJsonSerializer(typeof(T)); var ms = new MemoryStream(); dataSerializer.WriteObject(ms,item); ms.Position = 0; return new StreamReader(ms).ReadToEnd(); } public static T Deserialize<T>(string value) { var dataSerializer = new DataContractJsonSerializer(typeof(T)); return (T)dataSerializer. ReadObject(new MemoryStream (Encoding.UTF8.GetBytes(value))); } }
В приложении поставщике мы теперь можем сериализовать продукт и отправить указав идентификатор формата:
void Share_DataRequested(DataTransferManager sender, DataRequestedEventArgs args) { args.Request.Data.Properties.Title = "Product sharing"; var product = new Product() { Name = "Bread", Count = 2 }; var productJson = JsonSerializer.Serialize(product); args.Request.Data.SetData("product",productJson); }
Реализация «получателя»:
В приложении-получателе мы должны в манифесте указать что теперь мы поддерживаем формат product и можем использовать следующий код для получения продукта:
protected async override void OnNavigatedTo (NavigationEventArgs e) { if (e.Parameter != null) { var shareOperation = (ShareOperation)e.Parameter; if(shareOperation.Data.Contains("product")) { var productJson = await shareOperation.Data. GetTextAsync("product"); var product = JsonSerializer.Deserialize<Product>(productJson); txtBlock.Text = product.Name + " " + product.Count; } } }
Запустив приложение можно увидеть что наши данные были успешно переданы.
В данном случае мы передали и получили обычную строку в специальном виде. Мы по прежнему можем получить строку без указания формата GetTextAsync();
Стандартные «нестандартные типы». Данные на основе схем schema.org
Возможно вы захотите использовать не свой закрытый формат данных, а какой ни будь из общепринятых форматов.
К примеру, если есть другие приложения, которые работают с продуктами то имеет смысл организовать прием и передачу данных с этими приложениями.
Официально рекомендуется в таких случаях использовать схемы по ресурсу schema.org.
В первую очередь нам нужно определиться с типом сущности (книги, музыка, фильмы, продукты)
Выбрав соответствующую сущность, мы должны передавать или принимать в JSON строку согласно данным схемы.
Допустим мы выбрали сущность «Продукт» (http://schema.org/Product)
Реализация приложения поставщика:
Передача данных полностью аналогична передаче собственного формата рассмотренного выше, отличие только в формате JSON:
async void Share_DataRequested (DataTransferManager sender, DataRequestedEventArgs args { args.Request.Data.Properties.Title = "Product sharing"; var productFormat = @"{ ""type"" : ""http://schema.org/Product"", ""properties"" : { ""name"" : ""{0}"", ""description"" : ""{1}"", ""productID"" : ""{2}"" } }"; var productJson = String.Format (productFormat, "Olive oil", "description of product", "8410660101481"); args.Request.Data.SetData ("http://schema.org/Product",productJson); }
Реализация приложения приемника
Для поддержки этой схемы мы должны в манифесте указать формат данных schema.org/Product
Чтение данных осуществляется аналогично чтению строки которую мы рассматривали выше:
protected async override void OnNavigatedTo (NavigationEventArgs e) { if (e.Parameter != null) { shareOperation = (ShareOperation)e.Parameter; var productJson = await shareOperation. Data.GetTextAsync ("http://schema.org/Product"); JsonObject productObject = JsonObject.Parse(productJson); JsonObject properties = productObject["properties"].GetObject(); var productId = properties["productID"]; var productName = properties["name"]; var productDescriptions = properties["description"]; } }
Отправка и прием бинарных данных
В некоторых случаях проще передавать вместо строки бинарные данные. (К примеру у нас djvu редактор и мы хотим передавать данные в сыром виде).
Мы можем открыть файл передать ссылку на поток с открытого файла и прочитать этот поток в приложении приемнике.
Рассмотрим более интересный пример передачи данных из потока в памяти:
async void Share_DataRequested (DataTransferManager sender, DataRequestedEventArgs args) { args.Request.Data.Properties.Title = "Product sharing"; var product = new Product() { Name = "Bread", Count = 2 }; var stream = new InMemoryRandomAccessStream(); using(var writer=new DataWriter(stream)) { writer.WriteInt32(product.Name.Length); writer.WriteString(product.Name); writer.WriteInt32(product.Count); await writer.StoreAsync(); await writer.FlushAsync(); writer.DetachStream(); } stream.Seek(0); args.Request.Data.SetData("product", stream); }
В приложении-приемнике мы можем прочитать и разобрать этот поток.
protected async override void OnNavigatedTo (NavigationEventArgs e) { if (e.Parameter != null) { var shareOperation = (ShareOperation)e.Parameter; if (shareOperation.Data.Contains("product")) { var stream = await shareOperation.Data. GetDataAsync("product") as IRandomAccessStream; var product = new Product(); using(var streamReader=new DataReader(stream)) { await streamReader.LoadAsync((uint) stream.Size); var len = streamReader.ReadInt32(); product.Name = streamReader.ReadString((uint) len); product.Count = streamReader.ReadInt32(); } txtBlock.Text = product.Name + " " + product.Count; } } }
Отложенная отправка данных (Передача провайдера данных)
В предыдущем примере есть существенный недостаток. Мы инициализируем и подготавливаем данные всегда – даже если потом пользователь не воспользуется приложением и отменит операцию шаринга. В качестве альтернативы мы можем расшарить не данные а провайдер данных и приложение приемник может прочитать данные после того как он будет активирован пользователем. Переделаем последний пример с поставщиком данных с использованием провайдера данных.
async void Share_DataRequested (DataTransferManager sender, DataRequestedEventArgs args) { args.Request.Data.Properties.Title = "Product sharing"; args.Request.Data.SetDataProvider ("product",BinaryDataHandler); } private async void BinaryDataHandler (DataProviderRequest request) { try { var product = new Product() { Name = "Bread", Count = 2 }; var stream = new InMemoryRandomAccessStream(); using (var writer = new DataWriter(stream)) { writer.WriteInt32(product.Name.Length); writer.WriteString(product.Name); writer.WriteInt32(product.Count); await writer.StoreAsync(); await writer.FlushAsync(); writer.DetachStream(); } stream.Seek(0); request.SetData(stream); } finally { request.GetDeferral().Complete(); } }
Основное преимущество этого подхода заключается в том что теперь диалоговое окно шаринга появится быстрее, метод BinaryDataHandler будет вызван только ПОСЛЕ выбора конкретного приложения. И если пользователь отменит операцию шаринга метод не будет вызван вообще.
Правильная организация передачи больших объемов данных (музыка, видео)
В тех случаях когда приложение поставляет большой объем данных мы не должны заставлять пользователя ждать пока все данные не будут переданы.
В тех случаях когда мы уже завершили прием данных и если мы в интерфейсе провели все необходимые действия (к примеру нажали кнопку Send в почтовом интефейсе или кнопку Save где явно необходимо подтверждение от пользователя), мы можем вызвать метод:
shareOperation.ReportCompleted();
Который уведомляет систему что все операции по передаче данных завершены и приводит к закрытию интерфейса шаринга приложения приемника.
В тех случаях когда объем передаваемых данных слишком большой мы не должны заставлять пользователя ждать.
Мы можем вызывать метод
shareOperation.ReportStarted();
Перед извлечением большого пакета данных, который приведет к тому что процесс передачи начнется но при этом скроет интерфейс шаринга и предоставит пользователю возможность продолжать работу.
Далее мы должны вызывать метод
shareOperation.ReportDataRetreived();
Который уведомляет систему что мы уже извлекли все необходимые данные и исходное приложение можно освободить (на случай если пользователь его закрыл до этого).
По завершению передачи можем вызвать метод
shareOperation.ReportCompleted();
shareOperation.ReportError("error description");
Что бы уведомить систему об успешности или ошибке при передаче данных.
В последнем случае пользователь получит уведомление об ошибке и может посмотреть ошибку если снова откроет шаринг и перейдет по ссылке внизу что бы проверить состояние передачи.
В том случае, когда приложение для передачи данных использует BackgroundTransferTask, мы можем оповестить систему что используем такой способ передачи данных вызывав соответствующий метод:
shareOperation.ReportSubmittedBackgroundTask();
Добавление ссылок на панель шаринга.
Допустим в приложении приемнике после передачи данных приходится делать дополнительные операции. К примеру, когда мы передаем текст в почтовое приложение, пользователю надо еще выбрать адресата кому он отправит сообщение. В этом случае почтовое приложение создает быструю ссылку на последнее действие. В следующий раз выбрав эту быструю ссылку в поле «кому» будет заполнено соответствующей записью.
Предположим что в нашем приложении-приемнике нет единого списка продуктов. Есть список из списков продуктов. Если пользователь выбрал приложение и выбрал список куда хочет передать продукт мы можем предоставить быструю ссылку на этот список.
Рассмотрим как можно добавить быструю ссылку в наше приложение-приемнике:
private async void ButtonSaveLink_Click (object sender, RoutedEventArgs e) { var quickLinkInfo = new QuickLink { Id = "homeListId", Title = "Add to Home list", SupportedFileTypes = { "*" }, SupportedDataFormats = { //Оставлено для примера StandardDataFormats.Bitmap, "product" } }; try { var iconFile = await Package. Current.InstalledLocation.CreateFileAsync ("assets\\Logo.png", CreationCollisionOption.OpenIfExists); quickLinkInfo.Thumbnail = RandomAccessStreamReference. CreateFromFile(iconFile); shareOperation.ReportCompleted(quickLinkInfo); } catch (Exception) { shareOperation.ReportCompleted(); throw; } }
Соответствующий код в XAML
<Button Content="Save" Click="ButtonSaveLink_Click" />
Теперь если мы в приложении приемнике нажмем кнопку Save мы увидим что в следующий раз при шаринге у нас будет доступна быстрая ссылка.
Теперь в следующий раз мы можем упростить однотипное действие, определив по какому из быстрых ссылок щелкнул пользователь.
shareOperation = (ShareOperation)e.Parameter; if(shareOperation.QuickLinkId=="homeListId") { //handle selected list }
Почему не надо добавлять эту ссылку автоматически? Потому что пользователь возможно не выполнит никакого действия после активации приложения.
API шаринга специально спроектировано таким образом что добавление ссылки сворачивает приложение-приемник. Соответственно добавление ссылки должно быть последней операцией которую можно сделать. Т.е. если мы выбрали приложение приемник и выбрав список мы тут же сохраняемся и добавляем быструю ссылку. Или же выбрав список даем возможность изменить свой выбор и сохраняем в выбранный список только после нажатия кнопки Save.
В случае если ссылка теряет актуальность, мы можем удалить быструю ссылку из нашего приложения. (К примеру больше не существует списка с этим идентификатором).
shareOperation.RemoveThisQuickLink();
Обрабатываем логические ошибки.
Если страница приложения-источника не поддерживает шаринг данных при попытке шаринга пользователь увидит сообщение “This app can’t share.”
Возможно вы захотите подсказать пользователю что надо перейти в детали определенного продукта для шаринга. Или что он должен отметить несколько продуктов на этой странице для их шаринга (хотя как альтернатива можно было бы слать все данные если их не слишком много).
В любом случае, при необходимости вы можете уведомить пользователя почему вы не можете предоставить данные для поставки и что надо сделать пользователю.
Например:
async void Share_DataRequested(DataTransferManager sender, DataRequestedEventArgs args) { args.Request.Data.Properties.Title = "Product sharing"; args.Request.FailWithDisplayText ("please select products for sharing"); }
Теперь мы можем увидеть следующий результат при попытке «расшаривания» страницы:
Улучшаем шаринг данных. Сбор статистики
Сам по себе шаринг данных является практически универсальным API взаимодействия между приложениями. У нас как у разработчиков есть возможность сделать все что бы пользователям было максимально удобно.
Возможно вам захочется узнать, в какие приложения чаще всего передаются данные и из каких приложений чаще всего приходят данные.
В первом случае мы можем улучшить количество поддерживаемых форматов для приложения приемника или же в нашем приложении приемнике мы возможно захотим добавить более лучшую поддержку данных поступающих от наиболее популярного поставщика.
Узнать в какие приложения наиболее часто поставляются данные достаточно просто. Мы можем подписаться на событие выбора приложения и собирать статистику
protected override void OnNavigatedTo(NavigationEventArgs e) { DataTransferManager.GetForCurrentView().DataRequested += Share_DataRequested; DataTransferManager.GetForCurrentView(). TargetApplicationChosen += ShareText_TargetApplicationChosen; } protected override void OnNavigatingFrom (NavigatingCancelEventArgs e) { DataTransferManager.GetForCurrentView().DataRequested -= Share_DataRequested; DataTransferManager.GetForCurrentView(). TargetApplicationChosen -= ShareText_TargetApplicationChosen; } void ShareText_TargetApplicationChosen (DataTransferManager sender, TargetApplicationChosenEventArgs args) { var targetAppName = args.ApplicationName; //сбор статистики }
Точно так же в приложении приемнике мы можем узнать из какого приложения нам поставляются данные.
protected async override void OnNavigatedTo(NavigationEventArgs e) { if (e.Parameter != null) { var shareOperation = (ShareOperation)e.Parameter; //Название приложения поставщика var sourceAppName = shareOperation.Data.Properties.ApplicationName; //Url адрес приложения поставщика в маркете (null если не в маркете) var sourceAppMarketUrl = shareOperation.Data.Properties.ApplicationListingUri; //сбор статистики } }
Общие рекомендации по реализации шаринга. Избегаем ошибки в реализациях.
Не все изложенные здесь рекомендации являются официальными, часть рекомендаций я написал от себя и следует воспринимать критически.
Рекомендации для приложения-поставщика.
Не надо подписываться на событие запроса шаринга в конструкторе. Мы теряем возможность корректно отписаться от шаринга данных при переходе на другую страницу. Это приведет к тому что в многостраничном приложении шаринг будет срабатывать на всех страницах которые подписаны на шаринг, если пользователь побывал на этих страницах.
Наиболее корректное решение это подпись на шаринг при переходе на страницу и отписывание от шаринга при уходе с нее (Методы OnNavigateTo, OnNavigateFrom). Для простоты можно создать базову страницу в которой будет автоматизированы эти действия.
В качестве альтернативы можно подписаться на шаринг при активации приложения, в этом случае в зависимости от того, какая страница сейчас активна и каково его состояние поставлять данные из единого диспетчера шаринга (к примеру что бы не реализовывать подписку и отпуску на шаринг на каждой странице).
Желательно поставлять данные сразу в нескольких форматах. Даже если у вас эксклюзивный формат списка дел, предоставьте возможность поставки данных в Text или Html что бы пользователь мог отправить текущий список дел по почте, к примеру, своим друзьям.
Если ваше приложение передает большие объемы данных, то лучше использовать передачу провайдера данных, что бы максимально быстро запустить операцию шаринга.
При необходимости вы можете подсказывать пользователю что на текущей странице есть информация для поставки (к примеру если у вас игра или есть элементы игры и достижений в приложении, вы можете подсказать пользователю, что можно поделиться с друзьями через шаринг данных). Можно добавить кнопку-подсказку открывающую панель шаринга.
Следующий код приведет к тому что панель шаринга откроется, рекомендуется его использовать только если пользователь нажимает на кнопку в приложении.
DataTransferManager.ShowShareUI();
Рекомендации для приложения-приемника.
Учитывайте что в ваше приложение данные могут приходить сразу в нескольких форматах. Весь список доступных форматов можно получить из параметра: shareOperation.Data.AvailableFormats
Так же проверить доступность определенного формата можно методом shareOpertaion.Data.Contains();
При передаче больших объемов данных, таких как музыка, видео и т.д. используйте провайдер данных, и возможности shareOperation.Report…(); для того что бы выполнять передачу асинхронно и не заставлять пользователя ждать завершения выполнений операций.
Старайтесь использовать по мере необходимости добавление ссылок на панель шаринга для упрощения однотипных действий.
Если вы работаете со стандартами из scheme.org всегда допускайте что данные придут в неверном формате и проводите соответствующие проверки и обработку ошибок.
Автор статьи: Ахмед Шериев.