Based on the Windows Universal Samples, I am trying to populate a ListView
with a list of available Bluetooth Devices.
However, whilst the code I am using appears to pick up the devices I want to list, it does not populate the ListView
. I believe this is because I am attempting to bind the data incorrectly, but I am not sure what I am doing wrong.
I'd appreciate any help in resolving the issue
XAML
<Page
x:Class="Sample_Android_Connect.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Sample_Android_Connect"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Page.Resources>
<DataTemplate x:Key="ResultsListViewTemplate">
<Grid Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" MinWidth="100"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Height="40" Width="40" Margin="5" VerticalAlignment="Top">
<Image Source="{Binding Path=GlyphBitmapImage}"
Stretch="UniformToFill"/>
</Border>
<Border Grid.Column="1" Margin="5">
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Name:" Margin="0,0,5,0"/>
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold" TextWrapping="WrapWholeWords"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Id:" Margin="0,0,5,0"/>
<TextBlock Text="{Binding Path=Id}" TextWrapping="Wrap"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="CanPair:" Margin="0,0,5,0"/>
<TextBlock Text="{Binding Path=CanPair}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="IsPaired:" Margin="0,0,5,0"/>
<TextBlock Text="{Binding Path=IsPaired}"/>
</StackPanel>
</StackPanel>
</Border>
</Grid>
</DataTemplate>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="60" />
<RowDefinition />
</Grid.RowDefinitions>
<RelativePanel Background="#0063B1">
<TextBlock FontSize="30" Foreground="White" RelativePanel.AlignVerticalCenterWithPanel="True" RelativePanel.AlignHorizontalCenterWithPanel="True" Text="Bluetooth Devices" />
</RelativePanel>
<RelativePanel Padding="5" Grid.Row="1" Background="#0078D7">
<TextBlock Name="connectionStatus" FontSize="16" Foreground="White" Padding="2" FontWeight="Medium" RelativePanel.AlignHorizontalCenterWithPanel="True" Text="Connection Status: Not Connected" />
<Border BorderBrush="AntiqueWhite" BorderThickness="1" RelativePanel.Below="connectionStatus" Name="discoveredDevices" Width="500" Height="500" RelativePanel.AlignHorizontalCenterWithPanel="True" Margin="10">
<ListView x:Name="resultsListView"
ItemTemplate="{StaticResource ResultsListViewTemplate}"
ItemsSource="{Binding Path=ResultCollection}"
SelectionChanged="ResultsListView_SelectionChanged"
Height="500"
Width="500">
</ListView>
</Border>
<RelativePanel RelativePanel.Below="discoveredDevices" RelativePanel.AlignHorizontalCenterWithPanel="True">
<Button Name="connectButton" Content="Connect" Click="ConnectButton_Click" IsEnabled="False"/>
<Button Name="disconnectButton" RelativePanel.RightOf="connectButton" Content="Disconnect" Click="DisconnectButton_Click" IsEnabled="False"/>
</RelativePanel>
</RelativePanel>
</Grid>
</Grid>
</Page>
C#
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using Windows.Devices.Enumeration;
using Windows.Foundation;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Imaging;
namespace Sample_Android_Connect
{
public sealed partial class MainPage : Page
{
private DeviceWatcher deviceWatcher = null;
private TypedEventHandler<DeviceWatcher, DeviceInformation> handlerAdded = null;
private TypedEventHandler<DeviceWatcher, DeviceInformationUpdate> handlerUpdated = null;
private TypedEventHandler<DeviceWatcher, DeviceInformationUpdate> handlerRemoved = null;
private TypedEventHandler<DeviceWatcher, Object> handlerEnumCompleted = null;
private TypedEventHandler<DeviceWatcher, Object> handlerStopped = null;
public MainPage()
{
InitializeComponent();
ResultCollection = new ObservableCollection<DeviceInformationDisplay>();
StartWatcher();
}
public ObservableCollection<DeviceInformationDisplay> ResultCollection {
get;
private set;
}
private void ConnectButton_Click(object sender, RoutedEventArgs e)
{
Debug.WriteLine("Connect Clicked");
}
private void DisconnectButton_Click(object sender, RoutedEventArgs e)
{
Debug.WriteLine("Disconnect Clicked");
}
private void ResultsListView_SelectionChanged(object sender, RoutedEventArgs e)
{
Debug.WriteLine("Results List View Selection Changed");
}
private void StartWatcher()
{
ResultCollection.Clear();
// Get the device selector chosen by the UI then add additional constraints for devices that
// can be paired or are already paired.
//DeviceSelectorInfo deviceSelectorInfo = "";//(DeviceSelectorInfo)selectorComboBox.SelectedItem;
//string selector = "(" + deviceSelectorInfo.Selector + ")" + " AND (System.Devices.Aep.CanPair:=System.StructuredQueryType.Boolean#True OR System.Devices.Aep.IsPaired:=System.StructuredQueryType.Boolean#True)";
deviceWatcher = DeviceInformation.CreateWatcher(
"System.Devices.Aep.ProtocolId:=\"{e0cbf06c-cd8b-4647-bb8a-263b43f0f974}\"",
null, // don't request additional properties for this sample
DeviceInformationKind.AssociationEndpoint);
// Hook up handlers for the watcher events before starting the watcher
handlerAdded = new TypedEventHandler<DeviceWatcher, DeviceInformation>(async (watcher, deviceInfo) =>
{
// Since we have the collection databound to a UI element, we need to update the collection on the UI thread.
await Dispatcher.RunAsync(CoreDispatcherPriority.Low, () =>
{
ResultCollection.Add(new DeviceInformationDisplay(deviceInfo));
Debug.WriteLine("Watcher Added");
});
});
deviceWatcher.Added += handlerAdded;
handlerUpdated = new TypedEventHandler<DeviceWatcher, DeviceInformationUpdate>(async (watcher, deviceInfoUpdate) =>
{
// Since we have the collection databound to a UI element, we need to update the collection on the UI thread.
await Dispatcher.RunAsync(CoreDispatcherPriority.Low, () =>
{
// Find the corresponding updated DeviceInformation in the collection and pass the update object
// to the Update method of the existing DeviceInformation. This automatically updates the object
// for us.
Debug.WriteLine("Device Count: " + ResultCollection.Count);
foreach (DeviceInformationDisplay deviceInfoDisp in ResultCollection)
{
if (deviceInfoDisp.Id == deviceInfoUpdate.Id)
{
deviceInfoDisp.Update(deviceInfoUpdate);
// If the item being updated is currently "selected", then update the pairing buttons
DeviceInformationDisplay selectedDeviceInfoDisp = (DeviceInformationDisplay)resultsListView.SelectedItem;
if (deviceInfoDisp == selectedDeviceInfoDisp)
{
//UpdatePairingButtons();
}
break;
}
}
});
});
deviceWatcher.Updated += handlerUpdated;
handlerRemoved = new TypedEventHandler<DeviceWatcher, DeviceInformationUpdate>(async (watcher, deviceInfoUpdate) =>
{
// Since we have the collection databound to a UI element, we need to update the collection on the UI thread.
await Dispatcher.RunAsync(CoreDispatcherPriority.Low, () =>
{
// Find the corresponding DeviceInformation in the collection and remove it
foreach (DeviceInformationDisplay deviceInfoDisp in ResultCollection)
{
if (deviceInfoDisp.Id == deviceInfoUpdate.Id)
{
ResultCollection.Remove(deviceInfoDisp);
break;
}
}
Debug.WriteLine("Removed");
});
});
deviceWatcher.Removed += handlerRemoved;
handlerEnumCompleted = new TypedEventHandler<DeviceWatcher, Object>(async (watcher, obj) =>
{
await Dispatcher.RunAsync(CoreDispatcherPriority.Low, () =>
{
Debug.WriteLine("Completed");
});
});
deviceWatcher.EnumerationCompleted += handlerEnumCompleted;
handlerStopped = new TypedEventHandler<DeviceWatcher, Object>(async (watcher, obj) =>
{
await Dispatcher.RunAsync(CoreDispatcherPriority.Low, () =>
{
Debug.WriteLine("Stopped");
});
});
deviceWatcher.Stopped += handlerStopped;
deviceWatcher.Start();
}
public class DeviceInformationDisplay : INotifyPropertyChanged
{
private DeviceInformation deviceInfo;
public DeviceInformationDisplay(DeviceInformation deviceInfoIn)
{
deviceInfo = deviceInfoIn;
UpdateGlyphBitmapImage();
}
public DeviceInformationKind Kind {
get {
return deviceInfo.Kind;
}
}
public string Id {
get {
return deviceInfo.Id;
}
}
public string Name {
get {
return deviceInfo.Name;
}
}
public BitmapImage GlyphBitmapImage {
get;
private set;
}
public bool CanPair {
get {
return deviceInfo.Pairing.CanPair;
}
}
public bool IsPaired {
get {
return deviceInfo.Pairing.IsPaired;
}
}
public IReadOnlyDictionary<string, object> Properties {
get {
return deviceInfo.Properties;
}
}
public DeviceInformation DeviceInformation {
get {
return deviceInfo;
}
private set {
deviceInfo = value;
}
}
public void Update(DeviceInformationUpdate deviceInfoUpdate)
{
deviceInfo.Update(deviceInfoUpdate);
OnPropertyChanged("Kind");
OnPropertyChanged("Id");
OnPropertyChanged("Name");
OnPropertyChanged("DeviceInformation");
OnPropertyChanged("CanPair");
OnPropertyChanged("IsPaired");
UpdateGlyphBitmapImage();
}
private async void UpdateGlyphBitmapImage()
{
DeviceThumbnail deviceThumbnail = await deviceInfo.GetGlyphThumbnailAsync();
BitmapImage glyphBitmapImage = new BitmapImage();
await glyphBitmapImage.SetSourceAsync(deviceThumbnail);
GlyphBitmapImage = glyphBitmapImage;
OnPropertyChanged("GlyphBitmapImage");
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
Debug.WriteLine("PropertyChanged");
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
}
}
You need to set the DataContex
of your page, for example at the end of the constructor:
this.DataContext = this;
DataContext
is MVVM's way of knowing where should be the Bindings
searched for. The property is inherited down the XAML tree (unless you set a different DataContext
below), so you can set it for the page itself and it will work for all the page bindings.
Usually you actually create a separate ViewModel
class that creates a additional abstraction layer between the view and the model and where you expose the properties, ObservableCollections
and Commands
your view uses.
Using code behind as the view model is possible, but ideally you achieve the best separation of concerns if you create a dedicated ViewModel
class.