網絡通信
- 1. Socket通信
-
- (1) 基于UDP協定的通信
- (2) 通過TCP協定傳輸資料
- 2. HTTP協定通信
- 3. 背景傳輸
- 4. 推送通知
- 5. 通路RSS資源
- 6. 掃描Wi-Fi網絡
1. Socket通信
在Windows.Networking.Sockets命名空間下提供了支援Socket通信相關的類型。有趣的是,這些類型的命名中并沒有出現如TCP、UDP等關鍵詞,官方似乎有意避開這些傳統的命名方式,而是按照各通信協定的功能來命名。可參考如下:
- DatagramSocket ——用UDP協定的Socket網絡通信
- StreamSocket —— 通過流方式接收/發送網絡資料,實際上是基于TCP協定的Socket通信。在伺服器端,可以使用StreamSocketListener來監聽連接配接請求
- MessageWebSocket與StreamWebSocket —— 使用WebSocket相關技術進行網絡通信
(1) 基于UDP協定的通信
DatagramSocket類封裝了基于UDP資料報相關的網絡通信功能。由于UDP協定是無連接配接協定,資源消耗較少,處理效率高,經常被用于傳輸要求不太嚴格的資料,如聊天資訊、網絡視訊等。
在伺服器端,DatagramSocket類的使用步驟如下:
- 建立DatagramSocket執行個體。
- 處理MessageReceived事件,當收到新的消息時會引發該事件。
- 調用BindEndpointAsync方法綁定本地終結點,包括位址和端口;如果希望在本地任何位址上都監聽到資料,可以調用BindServiceNameAsync方法,該方法僅僅綁定本地端口。
而在用戶端,直接執行個體化一個DatagramSocket對象後,就可以直接通過GetOutputStreamAsync方法擷取一個輸出流對象以向遠端主機發送資料(直接将資料寫入該流即可),在調用時應指定遠端主機的位址(IP位址或主機名)和接收端口。
接下來通過一個示例來示範UDP協定通信的應用方法。本示例既可以作為監聽伺服器,也可以作為用戶端使用,是以可以在同一台機器上進行測試。本示例實作用戶端将文本消息發送到伺服器,伺服器将顯示收到的文本消息。
在頁面布局中,可以使用Pivot控件來添加兩個頁籤,一個用于伺服器,另一個用于用戶端。
<Pivot>
<PivotItem Header="伺服器">
<Grid RowSpacing="10">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel>
<TextBlock Text="在用戶端輸入以下IP位址:" FontSize="20"/>
<TextBlock x:Name="tbIp" FontSize="36" IsTextSelectionEnabled="True"/>
</StackPanel>
<ListView x:Name="lvMsg" Grid.Row="1">
<ListView.Header>
<TextBlock Foreground="LightPink" Text="收到的消息清單" FontSize="20"/>
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Margin="3,12">
<TextBlock FontSize="20" Foreground="Yellow">
來自
<Run Text="{Binding Path=FromIP}"/>
的消息:
</TextBlock>
<TextBlock TextWrapping="Wrap" FontSize="24" Text="{Binding Path=Message}"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</PivotItem>
<PivotItem Header="用戶端">
<StackPanel Spacing="10">
<TextBox x:Name="txtServer" Header="伺服器IP: "/>
<TextBox x:Name="txtMessage" Header="消息内容: " TextWrapping="Wrap" Height="160"/>
<Button HorizontalAlignment="Center" Content="發送" Tapped="Button_Tapped"/>
</StackPanel>
</PivotItem>
</Pivot>
為了在測試應用程式時能夠輕松得知伺服器的IP位址,可以擷取本地網絡的顯示名稱,并顯示在界面上。在頁面重寫的OnNavigatedTo方法中加入以下代碼:
protected async override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
//顯示伺服器的IP
var hosts = NetworkInformation.GetHostNames();
HostName svName = hosts.FirstOrDefault(h => h.Type == HostNameType.Ipv4 &&
h.IPInformation.NetworkAdapter.IanaInterfaceType == 6);
if (svName != null)
{
tbIp.Text = svName.DisplayName;
}
//...
}
GetHostNames方法會傳回多個主機名,随後通過FirstOrDefault擴充方法将裝置中的以太網卡連接配接篩選出來。h.Type == HostNameType.Ipv4表示隻選出v4版本的IP位址;NetworkAdapter.IanaInterfaceType屬性類型是一個整數值,此處數值6表示以太網卡,如果希望使用本地無線網卡進行通信,可以使用數值71進行篩選。
伺服器所使用的監聽端口号将通過一個常量值來固定:
/// <summary>
/// 用于接收資料的端口
/// </summary>
const string SERVICE_PORT = "795";
在類級别中聲明必要的變量,主要是兩個用于通信的Socket對象,一個用于伺服器,另一個用于用戶端。
/// <summary>
/// 用于伺服器的Socket
/// </summary>
DatagramSocket svrSocket = null;
/// <summary>
/// 用于用戶端的Socket
/// </summary>
在重寫的OnNavigatedTo方法中添加伺服器監聽實作:
protected async override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
//顯示伺服器的IP
var hosts = NetworkInformation.GetHostNames();
HostName svName = hosts.FirstOrDefault(h => h.Type == HostNameType.Ipv4 &&
h.IPInformation.NetworkAdapter.IanaInterfaceType == 6);
if (svName != null)
{
tbIp.Text = svName.DisplayName;
}
svrSocket = new DatagramSocket();
// 添加接收消息事件處理
svrSocket.MessageReceived += SerSocket_Received;
// 綁定到本地端口
await svrSocket.BindServiceNameAsync(SERVICE_PORT);
clientSocket = new DatagramSocket();
}
處理DatagramSocket對象的MessageReceived事件,顯示收到的消息。
async void SerSocket_Received(DatagramSocket sender,DatagramSocketMessageReceivedEventArgs args)
{
// 擷取相關的DataReader對象
DataReader reader = args.GetDataReader();
// 讀取消息内容
uint len = reader.UnconsumedBufferLength;
string msg = reader.ReadString(len);
// 遠端主機
string remoteHost = args.RemoteAddress.DisplayName;
reader.Dispose();
// 顯示接收到的消息
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal,
()=>
{
lvMsg.Items.Add(new { FromIP = remoteHost, Message = msg});
});
}
下面代碼實作用戶端向伺服器發送消息:
private async void Button_Tapped(object sender, TappedRoutedEventArgs e)
{
// 擷取輸出流
using (var outStream = await clientSocket.GetOutputStreamAsync(new HostName(txtServer.Text),SERVICE_PORT))
{
using (var writer = new DataWriter(outStream))
{
// 寫入流
writer.WriteString(txtMessage.Text);
//送出寫入的内容
await writer.StoreAsync();
}
}
}
在擷取到輸出流後,可以使用DataWriter類來向流對象寫入資料,使用該類所封裝的WriteString方法可以直接寫入字元串内容。預設情況下是使用UTF-8編碼格式,一般不需要修改。
要注意的是,在寫完資料後,要調用StoreAsync方法,被寫入的資料才會送出到輸出流中。
示例運作效果:UDP
使用UDP的另一篇部落格,可參考:添加連結描述
(2) 通過TCP協定傳輸資料
與UDP不同,TCP協定是基于連接配接的,即在通信之前,用戶端需要連接配接伺服器。這意味着TCP對資料的次序與完整性要求更為嚴格,確定資料準确無誤地達到目标終端。例如,檔案傳輸就應當使用TCP協定來完成,因為少一個位元組或者多一個位元組都有可能損壞檔案。
StreamSocket類封裝了基于TCP協定的Socket通信功能,在用戶端,通常遵循以下順序來使用StreamSocket類:
- 執行個體化StreamSocket對象。
- 調用ConnectAsync方法連接配接伺服器。
- 通過OutputStream屬性傳回的輸出流就可以發送資料;而InputStream屬性則傳回輸入流對象,用于接收資料。
- 當不再使用Socket時,調用Dispose方法釋放其占用的相關資源。
在伺服器中,則需要一個StreamSocketListener執行個體,綁定本地主機或某個端口,以監聽用戶端的連接配接請求。當有用戶端發出連接配接請求時,會引發ConnectionReceived事件,從事件參數中可以擷取一個與用戶端進行通信的StreamSocket執行個體。
下面示例将示範如何使用基于TCP協定的Socket通信。
本例将伺服器與用戶端合并在一個應用程式中,應用程式既可以充當伺服器角色,也可以用作用戶端。在用戶端選擇一個圖像檔案并輸入一些文本,應用程式會将圖像與文本一起發送到伺服器。随即伺服器會顯示收到的資料。
頁面布局XAML如下:
<Pivot>
<PivotItem Header="伺服器">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Spacing="10">
<TextBlock Text="伺服器IP位址:" Height="30"/>
<TextBlock x:Name="tbSvIP" FontSize="24" IsTextSelectionEnabled="True" Height="30"/>
</StackPanel>
<ListBox x:Name="lbItems" Grid.Row="1">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image Width="50" Height="50" Stretch="UniformToFill" Source="{Binding Path=Image}"/>
<TextBlock Grid.Column="1" TextWrapping="Wrap" FontSize="18" Text="{Binding Path=Text}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</PivotItem>
<PivotItem Header="用戶端">
<StackPanel Spacing="10">
<TextBox x:Name="txtServerIp" Header="伺服器位址:"/>
<Button Content="選擇圖像..." Tapped="OnPickImageFile"/>
<TextBox x:Name="txtContent" Header="說明文本:" Height="120"/>
<Button Content="發送" HorizontalAlignment="Stretch" Tapped="OnSend"/>
</StackPanel>
</PivotItem>
</Pivot>
在背景代碼中聲明StreamSocketListener執行個體,并處理ConnectionReceived事件:
protected async override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
//顯示伺服器的IP
var hosts = NetworkInformation.GetHostNames();
HostName svName = hosts.FirstOrDefault(h => h.Type == HostNameType.Ipv4 &&
h.IPInformation.NetworkAdapter.IanaInterfaceType == 6);
if (svName != null)
{
tbSvIP.Text = svName.DisplayName;
}
//執行個體化StreamSocketListener對象
listener = new StreamSocketListener();
//添加ConnectionReceived事件處理程式
listener.ConnectionReceived += Listener_ConnectionReceived;
//綁定本地端口
await listener.BindServiceNameAsync(LISTEN_PORT);
}
BindServiceNameAsync方法将監聽器綁定到一個本地端口,隻要發送到該端口的用戶端連接配接都會被接收。
當不再需要StreamSocketListener對象時,可以通過以下代碼将其釋放:
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
listener?.Dispose();
listener = null;
}
以下代碼處理ConnectionReceived事件:
async void Listener_ConnectionReceived(StreamSocketListener sender, StreamSocketListenerConnectionReceivedEventArgs args)
{
string text = string.Empty;
IRandomAccessStream imgStream = new InMemoryRandomAccessStream();
//處理接收到的連接配接
using (var socket = args.Socket)
{
using (DataReader reader = new DataReader(socket.InputStream))
{
try
{
//讀出第一個整數,表示圖檔檔案長度
await reader.LoadAsync(sizeof(uint));
uint len = reader.ReadUInt32();
await reader.LoadAsync(len);
IBuffer buffer = reader.ReadBuffer(len);
//寫入記憶體流
await imgStream.WriteAsync(buffer);
await reader.LoadAsync(sizeof(uint));
//讀出字元串的長度
len = reader.ReadUInt32();
//讀出字元串内容
if (len > 0)
{
await reader.LoadAsync(len);
text = reader.ReadString(len);
}
}
catch (Exception ex)
{
await new MessageDialog(ex.Message).ShowAsync();
}
}
//顯示接收到的内容
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
{
BitmapImage bmp = new BitmapImage();
bmp.DecodePixelWidth = 50;
imgStream.Seek(0);
await bmp.SetSourceAsync(imgStream);
lbItems.Items.Add(new { Image = bmp, Text = text });
});
}
}
使用DataReader類讀取資料時要先用LoadAsync方法從關聯的輸入流中加載一部分資料,然後再讀取。資料的讀取過程為:先讀出圖像檔案的大小,然後再根據這個大小值讀出圖像檔案;随後再讀出字元串的長度,最後根據長度将字元串讀出。
下面代碼用于發送資料:
IBuffer bufferImg;
private async void OnPickImageFile(object sender, TappedRoutedEventArgs e)
{
FileOpenPicker picker = new FileOpenPicker();
picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
picker.FileTypeFilter.Add(".jpg");
var imgFile = await picker.PickSingleFileAsync();
if (imgFile != null)
{
bufferImg = await FileIO.ReadBufferAsync(imgFile);
return;
}
bufferImg = null;
}
private async void OnSend(object sender, TappedRoutedEventArgs e)
{
if (txtServerIp.Text.Length == 0)
{
return;
}
if (bufferImg is null)
{
await new MessageDialog("請選擇圖像。").ShowAsync();
return;
}
var btn = sender as Button;
btn.IsEnabled = false;
using (StreamSocket socket = new StreamSocket())
{
try
{
//發起連接配接
await socket.ConnectAsync(new HostName(txtServerIp.Text),LISTEN_PORT);
//準備發送資料
using (DataWriter writer = new DataWriter(socket.OutputStream))
{
//首先寫入圖像資料
uint len = bufferImg.Length;
writer.WriteUInt32(len);
writer.WriteBuffer(bufferImg);
//接着寫入文本
if (txtContent.Text.Length == 0)
{
writer.WriteUInt32(0);
}
else
{
len = writer.MeasureString(txtContent.Text);
writer.WriteUInt32(len);
writer.WriteString(txtContent.Text);
}
await writer.StoreAsync();
}
}
catch (Exception ex)
{
await new MessageDialog(ex.Message).ShowAsync();
}
}
btn.IsEnabled = true;
}
發送資料時,可以通過DataWriter對象把内容寫入輸出流。過程是:先寫入圖像的檔案長度,随後寫入圖像檔案的内容;緊接着,寫入要發送文本的長度,最後才寫入文本内容。這樣做是為讓伺服器在接收資料時能夠準确地對資料進行分割。因為資料都是以位元組的形式進行傳輸的,圖像檔案的内容和文本内容在發送時會被合并到一起,如果不事先寫入每段資料的大小的話,在伺服器接收到資料後就無法判斷哪些位元組是屬于圖像檔案,哪些位元組是屬于文本的。
示例運作效果:TCP
2. HTTP協定通信
在網絡通信技術中,除了使用Socket外,還可以通過HTT協定直接通路Web伺服器上的内容,如從伺服器擷取内容或者送出内容到伺服器等。
在Windows.Web.Http命名空間下,公開了一系列支援各種HTTP通路的類型。其中,主要的元件是HttpClient類,使用該類可以向指定URI發出HTTP請求,并擷取從伺服器傳回的資料。無論是采取何種請求方式(比較常用的是GET和POST),HTTP通路過程都是遵循“一問一答”模式。用戶端應用程式向指定的URI送出請求可以了解為向伺服器“提出問題”,HTTP伺服器在收到請求後會進行處理,最後會向用戶端發出一條回複消息,可以了解為伺服器“回答問題”。
對于要發送到伺服器的資料,可以通過以下幾種類型來封裝(這些類型都實作IHttpContent接口):
- HttpStringContent : 直接将字元串作為内容發送。
- HttpBufferContent : 将緩沖區(IBuffer)内的資料發送到伺服器。
- HttpStreamContent : 資料以流的形式發送,如發送檔案。
- HttpFromUrlEncodedContent : 發送application/x-www-from-urlencoded MIME格式的資料,通常以POST方式送出。
- HttpMultipartContent 或 HttpMultipartFromDataContent : 主要用于包裝由多個部分組成的内容(multipart/* MIME或者multipart/from-data MIME格式)。例如發表一條含有圖檔的新微網誌。
下面示例将示範使用HttpClient類以GET方式請求網絡上的圖像檔案,并将擷取到的圖像顯示在頁面上。
頁面布局XAML如下:
<StackPanel Spacing="5">
<TextBox x:Name="txtUri" Header="請輸入圖像URI:"/>
<Button Content="擷取圖像" Tapped="OnGetImage"/>
<Image x:Name="img" Height="300" Stretch="Uniform"/>
</StackPanel>
在背景代碼中,處理Button的Tapped事件處理程式,從Web伺服器上擷取網絡圖像。
private async void OnGetImage(object sender, TappedRoutedEventArgs e)
{
using (HttpClient client = new HttpClient())
{
try
{
//直接向伺服器發送GET請求
HttpResponseMessage response = await client.GetAsync(new Uri(txtUri.Text));
//如果請求成功
if (response != null && response.StatusCode == HttpStatusCode.Ok)
{
//顯示獲得的圖像
BitmapImage bmp = new BitmapImage();
bmp.DecodePixelWidth = 400;
using (IRandomAccessStream stream = new InMemoryRandomAccessStream())
{
var str = response.Content.WriteToStreamAsync(stream);
stream.Seek(0);//将讀取位置設定為流的開始處
await bmp.SetSourceAsync(stream);
}
img.Source = bmp;
}
}
catch (Exception)
{
}
}
}
運作應用程式後,在文本框中輸入一個網路圖像的URI,然後單擊下方的按鈕,應用程式會下載下傳并顯示圖像。
3. 背景傳輸
背景傳輸一般用于上傳或者下載下傳體積較大的資料,如下載下傳或上傳檔案。要傳輸較小的資料,直接運用Socket或者HTTP傳輸即可。即使應用程式已退出,傳輸仍可以在背景進行(除非顯示取消傳輸或者出現錯誤)。而當應用程式被解除安裝時會連同與之關聯的背景任務一并删除。
背景傳輸分為兩類:上傳和下載下傳。相關的類型位于Windows.Networking.BackgroundTransfer命名空間下。
對于上傳操作,可以參考以下過程來實作:
- 執行個體化BackgroundUploader對象。
- 調用CreateUpload或者CreateUploadAsync方法建立一個UploadOperation對象執行個體,并從相應方法傳回。這兩種方法在調用時通常需要指定目标URI(要上傳到的伺服器位址)及要上傳的檔案。如果希望上傳多個檔案,可以調用帶有可以傳遞BackgroundTransferContentPart類型清單的重載版本。
- 調用上一步中獲得的UploadOperation對象的StartAsync方法就可以開始傳輸了。如果該傳輸任務已經在運作,則調用AttachAsync方法重新附加到目前應用程式中即可。
要在背景下載下傳内容,則可以參考以下過程:
- 執行個體化BackgroundDownloader對象。
- 調用CreateDownload或者CreateDownloadAsync方法建立DownloadOperation執行個體。
- 調用DownloadOperation執行個體的StartAsync方法開始傳輸;如果任務已經在運作,可以調用AttachAsync方法重新附加到目前應用程式。
接下來,将通過一個示例來示範背景傳輸的使用方法。本例将實作從用戶端将檔案上傳到伺服器程式。為了便于測試,伺服器通過一個Windows控制台應用程式(.Net Framework)承載WCF服務來實作。
首先,實作伺服器應用程式。服務協定定義如下:
[ServiceContract]
public interface IService
{
[OperationContract,WebInvoke(UriTemplate="/upload")]
void UploadFile(Stream content);
}
UploadFile方法必須聲明為具有一個Stream類型參數的服務方法,從用戶端上傳的檔案内容會傳遞給該參數,在服務類的實作代碼中,将從該參數中讀取用戶端上傳的檔案内容。
下面代碼實作服務協定:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class Service : IService
{
public async void UploadFile(Stream content)
{
string fileName = string.Empty;
IncomingWebRequestContext request = WebOperationContext.Current.IncomingRequest;
//從标頭擷取檔案名
fileName = request.Headers["fileName"];
if (string.IsNullOrEmpty(fileName))
{
Guid g = Guid.NewGuid();
fileName = g.ToString();
}
//開始接收檔案
try
{
//擷取使用者文檔庫位置
string docPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string newFilePath = Path.Combine(docPath, fileName);
//如果檔案存在,将其删除
if (File.Exists(newFilePath))
{
File.Delete(newFilePath);
}
using (FileStream outputStream = File.OpenWrite(newFilePath))
{
//從用戶端上傳的流中将資料複制到檔案流中
await content.CopyToAsync(outputStream);
}
Console.WriteLine($"在{DateTime.Now.ToLongTimeString()}成功接收檔案。");
//向用戶端發送回應消息
//WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.OK;
}
catch (Exception ex)
{
//處理錯誤
Console.WriteLine(ex.Message);
//WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.InternalServerError;
//WebOperationContext.Current.OutgoingResponse.StatusDescription = ex.Message;
}
}
}
檔案名通過一個名為fileName的HTTP标頭來傳遞,該标頭是自定義的,是以在用戶端開始上傳檔案時,需要指定該HTTP标頭。資料的讀取過程比較簡單,就是直接将參數傳遞進來的流中的内容複制到檔案流即可,檔案最終儲存到"文檔"目錄下。
服務類編寫完成後,在Main入口點處啟動WCF服務以接收用戶端上傳的資料。
class Program
{
static void Main(string[] args)
{
Uri localUri = new Uri("<服務基址>");
//開始運作WCF服務
WebServiceHost host = new WebServiceHost(typeof(Service), localUri);
//配置緩沖區的最大值
WebHttpBinding binding = new WebHttpBinding();
binding.MaxReceivedMessageSize = 500 * 1024 * 1024;
host.AddServiceEndpoint(typeof(IService), binding, "");
host.Opened += (a, b) => Console.WriteLine($"服務已啟動。\n上傳位址:{localUri}upload");
host.Closed += (a, b) => Console.WriteLine("closed服務已關閉。");
try
{
//打開服務
host.Open();
Console.WriteLine(host.State);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Task.Delay(30000).Wait(); //30s後關閉服務
host.Close();
Console.ReadLine();
}
}
應該将上面的<服務基址>替換為主機的真實位址,如192.168.1.12:8800,其中8800是端口号。在打開服務之前,需要修改WebHttpBinding類的MaxReceivedMessageSize屬性,以位元組為機關,可以依實際情況設定,因為預設的值太小,體積較大的檔案無法接收,是以必須手動更改該屬性的值。
下面将實作用戶端。
頁面布局XAML如下:
<StackPanel Spacing="10">
<TextBox x:Name="txtUploadUri" Header="請輸入上傳URI: "/>
<Button x:Name="btnUpload" Content="選擇檔案并開始上傳" Tapped="btnUpload_Tapped"/>
<ProgressBar x:Name="pb" Maximum="100" Minimum="0" Foreground="Yellow" Height="15"/>
<TextBlock x:Name="tbMsg" FontSize="20" TextWrapping="Wrap"/>
</StackPanel>
當導航進入頁面後,應先檢查一下是否有背景傳輸任務正在進行,如果有,則加入到目前程式中。
protected async override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
var uploadTasks = await BackgroundUploader.GetCurrentUploadsAsync();
if (uploadTasks.Count > 0)
{
UploadOperation uploadOpt = uploadTasks[0];
await SetUpload(uploadOpt,false);
}
}
調用BackgroundUploader類的GetCurrentUploadsAsync靜态方法,可以擷取目前正在執行的傳輸清單。
當按鈕被按下後,會通過檔案選擇器讓使用者選擇一個檔案,然後開始上傳。
private async void btnUpload_Tapped(object sender, TappedRoutedEventArgs e)
{
FileOpenPicker picker = new FileOpenPicker();
picker.SuggestedStartLocation = PickerLocationId.ComputerFolder;
picker.FileTypeFilter.Add("*");//表示所有檔案類型
var file = await picker.PickSingleFileAsync();
if (file != null)
{
BackgroundUploader uploader = new BackgroundUploader();
//設定檔案名
uploader.SetRequestHeader("fileName", file.Name);
uploader.Method = "POST";
//建立上傳任務
UploadOperation uploadOpt = uploader.CreateUpload(new Uri(txtUploadUri.Text.Trim()), file);
await SetUpload(uploadOpt,true);
}
}
以上多段代碼都調用了SetUpload方法,該方法為自定義方法,由于開始傳輸任務與附加傳輸任務的操作相似,故而統一封裝到SetUpload方法中。代碼如下:
/// <summary>
/// 執行背景上傳操作
/// </summary>
/// <param name="opr">操作背景傳輸任務的對象</param>
/// <param name="starting">是否為新的上傳任務</param>
async Task SetUpload(UploadOperation opr,bool starting)
{
//當上傳進度更新時能收到報告
IProgress<UploadOperation> progressReporter = new Progress<UploadOperation>(OnProgressHandler);
//啟動或附加任務
try
{
if (starting)
{
await opr.StartAsync().AsTask(progressReporter);
}
else
{
await opr.AttachAsync().AsTask(progressReporter);
}
}
catch (Exception ex)
{
var state = BackgroundTransferError.GetStatus(ex.HResult);
ShowMessage($"錯誤:{state}");
}
}
AsTask方法可以将異步操作轉換為基于Task的操作方式,并通過Progress類來接收進度更新。該Progress執行個體綁定一個OnProgressHandler方法以響應傳輸進度更新,實作代碼如下:
void OnProgressHandler(UploadOperation p)
{
BackgroundUploadProgress progress = p.Progress;
switch (progress.Status)
{
case BackgroundTransferStatus.Canceled:
ShowMessage("任務已取消。");
pb.Value = 0d;
break;
case BackgroundTransferStatus.Completed:
ShowMessage("任務已完成。");
pb.Value = 0d;
break;
case BackgroundTransferStatus.Error:
ShowMessage("發生錯誤。");
pb.Value = 0d;
break;
case BackgroundTransferStatus.Running:
double ps = progress.BytesSent * 100 / progress.TotalBytesToSend;
pb.Value = ps;
ShowMessage($"已上傳{progress.BytesSent}位元組,共{progress.TotalBytesToSend}位元組。");
break;
default:
break;
}
}
BackgroundUploadProgress結構封裝與處理進度相關的成員,例如已經上傳了多少個位元組、總共要上傳多少個位元組及目前狀态等。使用這些成員可以實時顯示上傳的進度。
最後是一個輔助方法,顯示提示消息:
void ShowMessage(string msg)
{
tbMsg.Text = msg;
}
以管理者身份啟動VS 2019(WCF項目需要管理者權限才可正常運作),運作示例程式(伺服器端與用戶端都要運作),在用戶端上輸入伺服器位址,然後選擇一個檔案,開始上傳。
示例運作效果:背景傳輸
4. 推送通知
傳統的聯網通知是裝置用戶端定期通路網絡來查詢是否有新的資訊提供給使用者,但這中方案消耗網絡資源較多,而且,如果使用者使用按流量計費的上網方式,勢必會耗掉許多不必要的網絡流量。是以,推送通知就伴随雲技術而誕生,應用開發者首先将通知内容發送到微軟公司的雲伺服器上(稱為推送伺服器),再由推送伺服器将消息轉發到使用者的裝置上,即使使用者暫時沒有聯網,推送伺服器會将消息保留一段時間。
推送通知使用戶端程式不再通過主動查詢的方式進行更新,而是主動等待消息的到來,這種方案可以大大節省網絡資源的開銷,同時不會浪費過多的資料流量。
推送伺服器是通過為每個應用程式配置設定一個唯一的URI來确定将通知轉發給目标使用者的,稱為推送通道。是以,用戶端應用程式在運作後,需要向推送伺服器申請一個URI,并且還應該将該URI告訴開發者,一般可以通過各種網絡技術将URI發送到開發者伺服器。在得到通道URI後,開發者隻需将通知的内容(通知内容實質上是XML文檔)以HTTP POST方式發送到該通道URI,推送伺服器就能識别出是發送給哪個用戶端的了。
在用戶端應用開發中,與推送通知相關的API都被放到Windows.Networking.PushNotifications命名空間下。調用PushNotificationChannelManager類的CreatePushNotificationChannelForApplicationAsync靜态方法可以傳回一個PushNotificationChannel對象執行個體,該執行個體包含推動通道的URI,當應用程式不希望接收推送通知時,可以調用Close方法關閉通道。
接下來,将通過一個完整的示例來示範實作推送通知的過程。
(1) 建立用戶端應用程式,頁面布局XAML如下:
<StackPanel>
<TextBlock TextWrapping="Wrap" FontSize="20">
以下開關預設處于關閉狀态,當開關打開時,會向推送伺服器申請通道URI,并顯示在VS2019的"輸出"視窗中。
<LineBreak/>
如果不希望接收推送通知,使以下控件處于關閉狀态即可。
</TextBlock>
<ToggleSwitch IsOn="False" Toggled="ToggleSwitch_Toggled"/>
</StackPanel>
處理ToggleSwitch控件的Toggled事件,當開關控件的狀态發生改變後做出響應。
PushNotificationChannel channel = null;
private async void ToggleSwitch_Toggled(object sender, RoutedEventArgs e)
{
ToggleSwitch tgswitch = sender as ToggleSwitch;
if (tgswitch.IsOn)
{
//申請通道
channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
//輸出URI
Debug.WriteLine($"推送通道:{channel.Uri}");
}
else
{
//關閉通道
channel?.Close();
channel = null;
}
}
當開關控件處于打開狀态時,申請通道URI;否則關閉通道。當通道被關閉後,應用程式就不能接收推送通知了。
(2) 隻有将目前應用程式項目與商店對應産品進行關聯,才可以申請到有效的通道URI。是以需要使用開發者賬号登入Windows開發人員中心,進入"儀表闆",建立産品,由于這隻是測試demo,不用釋出到商店,故不用上傳軟體包。之後在VS 2019中,将目前項目與商店關聯。
(3) 選中下圖中标記的選項,會導航進相關頁面,得到自動配置設定的sid和秘鑰,後面用于發送通知的用戶端兌換通路令牌時會用到該sid和秘鑰。這個秘鑰隻需開發者自己知道即可,不要對他人公開。如果該秘鑰不慎洩露,可以重新擷取,否則一些不法分子可能會利用該秘鑰來冒充開發者發送垃圾廣告或虛假資訊。
(4) 接下來還要開發發送推送通知的示例用戶端。本例将通過一個UWP應用程式來完成。
頁面布局XAML如下:
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock>擷取Token</TextBlock>
<Grid BorderThickness="1" BorderBrush="GhostWhite" Margin="20">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBox x:Name="tbSid" AcceptsReturn="True" Header="SID:" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="10"/>
<PasswordBox x:Name="tbKey" PasswordChar="*" Header="用戶端密鑰:" Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="10"/>
<Button x:Name="tbnToken" Content="擷取通路令牌" Grid.Column="1" VerticalAlignment="Center"
HorizontalAlignment="Stretch" Margin="10" Tapped="tbToken_Tapped"/>
<TextBox x:Name="tbToken" TextWrapping="Wrap" IsReadOnly="True" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="10"/>
</Grid>
<TextBlock Grid.Row="1">發送通知</TextBlock>
<Grid Grid.Row="1" Margin="20" BorderThickness="1" BorderBrush="GhostWhite">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center">通道URI:</TextBlock>
<TextBox x:Name="tbUri" Grid.Column="1" HorizontalAlignment="Stretch"/>
</Grid>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button x:Name="bntSent" Content="發送通知" Margin="10" HorizontalAlignment="Stretch"
Tapped="bntSent_Tapped"/>
<TextBox x:Name="tbHeader" Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Margin="10" IsReadOnly="True"/>
<TextBox x:Name="tbContent" Grid.RowSpan="2" Grid.Column="1" Header="通知内容"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="10"
AcceptsReturn="True"/>
</Grid>
</Grid>
</Grid>
(5) 以下代碼實作向推送伺服器索取通路令牌。
AccessTokenData tokendt = null;
async Task<AccessTokenData> RequestTokenAsync(string sid, string pk)
{
//驗證身份并擷取令牌的URI
Uri reqUri = new Uri("https://login.live.com/accesstoken.srf");
try
{
using (HttpClient client = new HttpClient())
{
//POST的内容必須為application/x-www-from-urlencoded格式
Dictionary<string, string> formdata = new Dictionary<string, string>
{
{ "grant_type","client_credentials"},
{ "client_id",sid},
{ "client_secret",pk},
{ "scope","notify.windows.com"}
};
FormUrlEncodedContent content = new FormUrlEncodedContent(formdata);
//發送請求
HttpResponseMessage response = null;
response = await client.PostAsync(reqUri, content);
if (response != null && response.StatusCode == System.Net.HttpStatusCode.OK)
{
//反序列化伺服器回應的資料
using (Stream stream = await response.Content.ReadAsStreamAsync())
{
DataContractJsonSerializer sz = new DataContractJsonSerializer(typeof(AccessTokenData));
tokendt = sz.ReadObject(stream) as AccessTokenData;
}
}
else
{
await new MessageDialog("發送失敗").ShowAsync();
}
}
}
catch (Exception ex)
{
await new MessageDialog(ex.Message).ShowAsync();
}
return tokendt;
}
名為tbToken的Button控件的事件處理程式:
private async void tbToken_Tapped(object sender, TappedRoutedEventArgs e)
{
if (string.IsNullOrEmpty(tbSid.Text) || string.IsNullOrEmpty(tbKey.Password))
{
return;
}
var token = await RequestTokenAsync(tbSid.Text, tbKey.Password);
tbToken.Text = token.AccessToken;
}
其中AccessTokenData是一個自定義類,後面反序列化推送伺服器傳回的json對象時會用到。
申請Token的URI為添加連結描述,在向伺服器送出的字段中,client_id字段指的是應用在應用商店中的SID值,client_secret字段對應的是用戶端秘鑰,送出方式必須為POST。
向推送伺服器發送通知隻需要用到access_token對應的值即可。
處理請求傳回的JSON對象,最簡單的方法是聲明一個可序列化的類,并将成員的命名與JSON對象的字段比對,最後使用JSON反序列化的方式直接讀出。
[DataContract]
public class AccessTokenData
{
[DataMember(Name ="access_token")]
public string AccessToken { get; set; }
[DataMember(Name ="token_type")]
public string TokenType { get; set; }
}
得到通路令牌後,就可以使用它來發送通知了。
private async void bntSent_Tapped(object sender, TappedRoutedEventArgs e)
{
HttpWebRequest req = WebRequest.CreateHttp(tbUri.Text.Trim());
req.Method = "POST";
//添加驗證頭
req.Headers.Add("Authorization", $"bearer {tokendt.AccessToken}");
req.ContentType = "text/xml";
//該HTTP标頭表示通知類型為Toast
req.Headers.Add("X-WNS-Type", "wns/toast");
//寫入通知内容
byte[] data = Encoding.UTF8.GetBytes(tbContent.Text);
using (Stream stream = req.GetRequestStream())
{
await stream.WriteAsync(data, 0, data.Length);
}
try
{
//發送請求
HttpWebResponse response = req.GetResponse() as HttpWebResponse;
//輸出響應頭
StringBuilder strbd = new StringBuilder();
foreach (var hd in response.Headers.AllKeys)
{
strbd.AppendLine($"{hd}: {req.Headers.Get(hd)}");
}
tbHeader.Text = strbd.ToString();
if (response.StatusCode == HttpStatusCode.OK)
{
await new MessageDialog("發送成功").ShowAsync();
}
else
{
await new MessageDialog("發送失敗").ShowAsync();
}
}
catch (Exception ex)
{
tbHeader.Text = ex.Message;
}
}
本示例僅僅示範了Toast通知的發送。其他類型通知的發送方法相似,隻是表示通知内容的XML文檔不同而已。各類通知的XML模闆都可以在"Windows開發人員中心"的參考文檔中找到。
示例運作效果視訊:WNS(Toast)通知
5. 通路RSS資源
RSS(Really Simple Syndication)被廣泛用于資訊共享和網站内容同步釋出等多種網絡資源方案中,它基于XML标準,可以在多種平台和終端之間傳遞。比較常見的RSS資源有新聞頻道、部落格、軟體更新等。
Windows.Web.Syndication命名空間提供了支援通路并分析RSS資源的API。使用這些公開的類型,可以輕松地檢索RSS内容,開發者不需要手動去分析描述RSS資源的XML文檔。
SyndicationClient類支援對RSS伺服器的通路,在建立對象後,調用RetrieveFeedAsync方法可以從指定URI處擷取RSS文檔,傳回類型為SyndicationFeed。該類對RSS文檔的結構進行封裝,并可以從它的Items屬性得到一個SyndicationItem清單。SyndicationItem表示一條資料記錄。
下面示例将示範如何通路RSS資源。
應用程式的頁面布局XAML如下:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="輸入URI:" VerticalAlignment="Center"/>
<TextBox x:Name="txtUri" Grid.Column="1"/>
<Button Content="擷取" Margin="12,0,0,0" Grid.Column="2" Tapped="OnGetData"/>
</Grid>
<Grid Grid.Row="1" Margin="0,18,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ListView x:Name="lvItems" IsItemClickEnabled="True" ItemClick="lvItems_ItemClick">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Foreground="Purple" Text="{Binding Title.Text}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<WebView x:Name="wv" Grid.Column="1"/>
</Grid>
</Grid>
在背景代碼中,首先處理"擷取"按鈕的Tapped事件,從指定URI處請求RSS資源。
private async void OnGetData(object sender, TappedRoutedEventArgs e)
{
SyndicationClient client = new SyndicationClient();
//開始請求RSS資源
try
{
SyndicationFeed feed = await client.RetrieveFeedAsync(new Uri(txtUri.Text.Trim()));
if (feed != null)
{
await new MessageDialog(feed.Items.Count.ToString()).ShowAsync();
lvItems.ItemsSource = feed.Items;
}
}
catch (Exception ex)
{
await new MessageDialog(ex.Message).ShowAsync();
}
}
直接将SyndicationFeed對象的Items屬性指派給ListView控件的ItemsSource屬性,可以在ListView控件中顯示RSS文檔的所有内容清單。
在前面的XAML代碼中,ListView控件自定義了子項的資料模闆:
資料模闆中包含一個TextBlock控件,其Text屬性使用資料綁定來指派。由于ItemsSource屬性引用的是SyndicationFeed對象的Items清單,即每一個ListView子項所對應的資料對象為單個SyndicationItem執行個體,而SyndicationItem類的Title屬性又是一個實作了ISyndicationText接口的類型執行個體。再從Title.Text屬性中就能得到目前資料記錄的标題内容。是以,Binding擴充标記的Path路徑就是Title.Text。
處理ListView控件的ItemClick事件(必須将IsItemClickEnabled屬性設定為ture才能引發該事件),當清單中的某個項被單擊時觸發事件,并在WebView控件中顯示資料記錄的詳細内容。代碼如下:
private void lvItems_ItemClick(object sender, ItemClickEventArgs e)
{
SyndicationItem feedItem = e.ClickedItem as SyndicationItem;
if (feedItem != null)
{
//如果Content屬性為null,則擷取Summary屬性的内容
if (feedItem.Content is null)
{
wv.NavigateToString(feedItem.Summary.Text);
}
else
{
wv.NavigateToString(feedItem.Content.Text);
}
}
}
運作應用程式示例,然後在文本框中輸入一個RSS位址(如某個新聞頻道的RSS位址),然後單擊"擷取"按鈕,當資料擷取成功後,會在清單控件中顯示資料記錄清單。單擊某個項就可以檢視其具體内容,如下圖所示。
6. 掃描Wi-Fi網絡
使用WiFiAdapter類可以在應用程式中掃描附近的無線網絡。要對Wi-Fi網絡進行掃描,首先應當擷取目前裝置上可用的擴充卡(無線網卡),擷取方法有兩種:最簡單的方法是直接調用WiFiAdapter類的FindAllAdaptersAsync方法,它是一個靜态方法,可以直接通路,調用完成後會傳回一個WiFiAdapter清單,其中每一個WiFiAdapter執行個體映射到一張無線網卡上。通常裝置隻配置一張無線網卡,是以多數情況下該清單中隻有一個元素,但一台裝置也可能存在配置多張無線網卡的情況,為了針對不同的情況,FindAllAdaptersAsync方法以清單形式傳回結果。另一種擷取無線擴充卡的方法是調用GetDeviceSelector方法,得到一個用于裝置篩選的AQS字元串(進階查詢字元串),然後調用 Windows.Devices.Enumeration.DeviceInformation.FindAllAsync方法來執行篩選,進而得到一個裝置資訊清單(由DeviceInformation類型組成的集合),最後再通過WiFiAdapter.FromIdAsync方法從裝置的ID産生WiFiAdapter執行個體。
有了WiFiAdapter執行個體後,就可以調用ScanAsync方法開始對附近的無線網絡進行掃描。掃描結束後,會把掃描的結果存放在NetworkReport屬性中,該屬性引用一個WiFiNetworkReport類型的對象,通路該對象的AvailableNetworks屬性就能得到一個由WiFiAvailableNetwork對象組成的清單。WiFiAvailableNetwork類封裝了與單個有效無線網絡相關的資訊,比如SSID、網絡類型、信号強度等。
WiFiAdapter類除了可以掃描無線網絡外,還可以對已經掃描出來的網絡進行連接配接。要連接配接指定無線網絡,可以調用ConnectAsync方法,将表示目标網絡的WiFiAvailableNetwork對象作為參數傳入即可;如果要斷開已經連接配接的網絡,調用Disconnect方法。
下面将通過一個示例來示範如何掃描Wi-Fi網絡。具體步驟如下:
(1) 建立應用程式項目。
(2) 應用程式的頁面布局XAML如下:
<Grid Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ComboBox x:Name="cmbAdts" Header="無線網卡" Margin="1,6" DisplayMemberPath="Name"
SelectionChanged="cmbAdts_SelectionChanged"/>
<ListView x:Name="lvNetworks" Grid.Row="1" Margin="2" SelectionChanged="lvNetworks_SelectionChanged"
SelectionMode="Single">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Margin="1,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<TextBlock Text="SSID: " FontSize="20"/>
<TextBlock Grid.Column="1" FontSize="20" FontWeight="Bold" Text="{Binding Ssid}"/>
<TextBlock Grid.Row="1" Text="信号強度: "/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding SignalBars}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
ComboBox控件用來顯示目前裝置上可用的無線網卡清單,使用者可以從中選擇一張網卡,然後在這張網卡上對無線網絡進行掃描,掃描的結果将顯示在ListView控件中。
(3) 自定義一個WiFiAdapterItem類,用來封裝與無線網卡相關的資訊。
public class WifiAdapterItem
{
WiFiAdapter mWifiadt;
public WifiAdapterItem(WiFiAdapter adt) => mWifiadt = adt;
public WiFiAdapter Adapter => mWifiadt;
public string Name => mWifiadt.NetworkAdapter.NetworkAdapterId.ToString();
}
在該類中,公開了一個Adapter屬性,其他的調用代碼可以通過這個屬性來獲得關聯的WiFiAdapter執行個體的引用。
(4) 處理應用程式頁面的Loaded事件,代碼如下:
private async void Page_Loaded(object sender, RoutedEventArgs e)
{
//首先判斷一下應用程式是否有通路無線擴充卡的權限
var res = await WiFiAdapter.RequestAccessAsync();
if (res != WiFiAccessStatus.Allowed)
{
await new MessageDialog("操作被拒絕。").ShowAsync();
return;
}
//擷取無線網卡清單
var wifiAdts = await WiFiAdapter.FindAllAdaptersAsync();
var itemList = new List<WifiAdapterItem>();
foreach (var adt in wifiAdts)
{
var item = new WifiAdapterItem(adt);
itemList.Add(item);
}
//将清單綁定到ComboBox控件上
cmbAdts.ItemsSource = itemList;
if (itemList.Count > 0)
{
cmbAdts.SelectedIndex = 0;
}
}
無法保證應用程式都具備通路無線網卡的權限,是以,在擷取擴充卡清單之前,應該先用RequestAccessAsync方法檢查一下,應用程式是否有通路權,隻要方法傳回的是WiFiAccessStatus.Allowed以外的值,都說明應用程式沒有通路權限。
(5) 處理ComboBox控件的SelectionChanged事件,當下拉清單框中的選擇項發生更改後,立即使用被選中的無線擴充卡進行無線網絡掃描。
WiFiAdapter adapter = null;
private async void cmbAdts_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var itemInfo = cmbAdts.SelectedItem as WifiAdapterItem;
adapter = itemInfo.Adapter;
await adapter.ScanAsync();
IReadOnlyList<WiFiAvailableNetwork> nwItems = adapter.NetworkReport?.AvailableNetworks;
if (nwItems != null && nwItems.Count > 0)
{
lvNetworks.ItemsSource = nwItems;
}
}
注意ScanAsync方法是支援異步等待的方法,在使用時,一定要用await運算符來等待掃描完成,否則,NetworkReport中的AvailableNetworks清單裡面可能不存在任何資料,擷取空白的清單沒有實際意義。
(6) 要讓應用程式具有通路無線網絡擴充卡的權限,必須配置清單檔案。打開清單檔案,在Capabilities元素下面加入以下配置:
<Capabilities>
<Capability Name="internetClient" />
<DeviceCapability Name="wifiControl"/>
</Capabilities>
加入wifiControl聲明後,應用程式才能通路Wi-Fi網絡相關的API。
示例的運作結果如下圖所示。