Last active
July 18, 2016 06:37
-
-
Save kentcb/63c06262b2a36e21fa94873c7fff76dc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public delegate DeviceViewModel DeviceViewModelFactory(IObservable<IDevice> device, IDeviceMetadata deviceMetadata, IObservable<Unit> timer); | |
public sealed class DeviceViewModel : ReactiveObject | |
{ | |
public DeviceViewModel( | |
IObservable<IDevice> device, | |
IDeviceMetadata deviceMetadata, | |
IScheduler scheduler, | |
IObservable<Unit> timer) | |
{ | |
... | |
} | |
public string Name => ...; | |
public string FriendlyName => ...; | |
public bool IsAutoConnectEnabled => ...; | |
public bool IsConnected => ...; | |
public DateTime? LastConnected => ...; | |
public SignalStrength SignalStrength => ...; | |
} | |
public sealed class ConnectionsViewModel : ReactiveObject, ISupportsActivation | |
{ | |
private readonly ViewModelActivator activator; | |
private readonly ReactiveCommand<Unit, Unit> scanCommand; | |
private readonly IList<GroupViewModel<string, DeviceViewModel>> groupedDevices; | |
private ReadOnlyObservableCollection<DeviceViewModel> devices; | |
private ReadOnlyObservableCollection<DeviceViewModel> autoConnectedDevices; | |
private ReadOnlyObservableCollection<DeviceViewModel> manualConnectedDevices; | |
private IImmutableList<IDeviceMetadata> deviceMetadata; | |
public ConnectionsViewModel( | |
IBluetoothService bluetoothService, | |
IDeviceMetadataService deviceMetadataService, | |
DeviceViewModelFactory deviceViewModelFactory, | |
ILogger logger, | |
IScheduler scheduler) | |
{ | |
this.activator = new ViewModelActivator(); | |
var devicesCache = new SourceCache<IDevice, string>(device => device.Name); | |
var deviceViewModelsWithMetadata = new SourceList<DeviceViewModel>(); | |
var deviceViewModelsWithoutMetadata = new SourceList<DeviceViewModel>(); | |
var deviceViewModels = deviceViewModelsWithMetadata | |
.Connect() | |
.Or(deviceViewModelsWithoutMetadata.Connect()); | |
// TODO: sorting here isn't working | |
var autoConnectedDeviceViewModels = deviceViewModels | |
.FilterOnProperty(device => device.IsAutoConnectEnabled, device => device.IsAutoConnectEnabled) | |
.Sort(DeviceViewModelComparer.Instance) | |
.Bind(out this.autoConnectedDevices) | |
.Subscribe(); | |
// TODO: sorting here isn't working | |
var manualConnectedDeviceViewModels = deviceViewModels | |
.FilterOnProperty(device => device.IsAutoConnectEnabled, device => !device.IsAutoConnectEnabled) | |
.Sort(DeviceViewModelComparer.Instance) | |
.Bind(out this.manualConnectedDevices) | |
.Subscribe(); | |
this.groupedDevices = new List<GroupViewModel<string, DeviceViewModel>> | |
{ | |
new GroupViewModel<string, DeviceViewModel>("Automatic", this.autoConnectedDevices), | |
new GroupViewModel<string, DeviceViewModel>("Manual", this.manualConnectedDevices) | |
}; | |
deviceViewModels | |
.Bind(out this.devices) | |
.Subscribe(); | |
this.scanCommand = ReactiveCommand | |
.CreateFromObservable( | |
() => | |
{ | |
devicesCache.Clear(); | |
return bluetoothService | |
.GetAvailableDevices() | |
.Where(device => device.Name != null) | |
.Do(devicesCache.AddOrUpdate) | |
.Select(_ => Unit.Default) | |
.Timeout(TimeSpan.FromMinutes(2), scheduler); | |
}, | |
outputScheduler: scheduler); | |
this | |
.WhenActivated( | |
disposables => | |
{ | |
// retrieve metadata each time we activate | |
deviceMetadataService | |
.GetAll() | |
.Do(deviceMetadata => this.DeviceMetadata = deviceMetadata) | |
.Subscribe() | |
.AddTo(disposables); | |
// dictates how often each child view model updates its display (necessary because some of the information displayed is time sensitive) | |
var publishedTimer = Observable | |
.Timer(TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3), scheduler) | |
.Select(_ => Unit.Default) | |
.Publish(); | |
var timer = publishedTimer | |
.StartWith(Unit.Default); | |
this | |
.WhenAnyValue(x => x.DeviceMetadata) | |
.Where(deviceMetadatas => deviceMetadatas != null) | |
.Do( | |
deviceMetadatas => | |
{ | |
deviceViewModelsWithMetadata.Clear(); | |
foreach (var deviceMetadata in deviceMetadatas) | |
{ | |
// TODO: really not loving this. Probably a simpler way? I just need to pick out the appropriate device from the cache | |
// for each VM. Every time a device is added to the cache, it needs to tick through to the appropriate VM. As this | |
// code stands, it's quite inefficient because every VM will lookup its device every time any change is made to the | |
// cache. | |
var device = devicesCache | |
.Connect() | |
.Select(_ => devicesCache.Lookup(deviceMetadata.Name)) | |
.Where(lookupResult => lookupResult.HasValue) | |
.Select(lookupResult => lookupResult.Value) | |
.FirstAsync(); | |
deviceViewModelsWithMetadata.Add(deviceViewModelFactory(device, deviceMetadata, timer)); | |
} | |
}) | |
// now that we have all the metadata, kick off a scan | |
.SelectMany(_ => this.ScanCommand.Execute()) | |
.Subscribe() | |
.AddTo(disposables); | |
deviceViewModelsWithoutMetadata.Clear(); | |
// devices may be discovered that do not yet have any metadata in our database. In this case, we want to add a VM without metadata | |
// obviously the use of Any stinks and I'd love to improve it. However, using a SourceCache for VMs is proving problemmatic because | |
// the device name is obtained asynchronously. Since the name is also the key, the key can be null temporarily | |
devicesCache | |
.Connect() | |
.Filter(device => !this.devices.Any(deviceViewModel => deviceViewModel.Name == device.Name)) | |
.Transform(device => deviceViewModelFactory(Observable.Return(device), null, timer)) | |
.SelectMany(changes => changes) | |
.Do( | |
change => | |
{ | |
// TODO: either find a better way to do this, or might have to handle other reasons too | |
if (change.Reason == ChangeReason.Add) | |
{ | |
deviceViewModelsWithoutMetadata.Add(change.Current); | |
} | |
else | |
{ | |
throw new NotSupportedException($"Unsupported change reason: {change.Reason}."); | |
} | |
}) | |
.Subscribe() | |
.AddTo(disposables); | |
publishedTimer | |
.Connect() | |
.AddTo(disposables); | |
}); | |
} | |
public ViewModelActivator Activator => this.activator; | |
public ReactiveCommand<Unit, Unit> ScanCommand => this.scanCommand; | |
public ReadOnlyObservableCollection<DeviceViewModel> Devices => this.devices; | |
// Xamarin.Forms requires a list of lists | |
public IList<GroupViewModel<string, DeviceViewModel>> GroupedDevices => this.groupedDevices; | |
private IImmutableList<IDeviceMetadata> DeviceMetadata | |
{ | |
get { return this.deviceMetadata; } | |
set { this.RaiseAndSetIfChanged(ref this.deviceMetadata, value); } | |
} | |
private sealed class DeviceViewModelComparer : IComparer<DeviceViewModel> | |
{ | |
public static readonly DeviceViewModelComparer Instance = new DeviceViewModelComparer(); | |
private DeviceViewModelComparer() | |
{ | |
} | |
public int Compare(DeviceViewModel x, DeviceViewModel y) | |
{ | |
// sort signal strength DESC, last connected DESC, name ASC | |
var signalCompare = x.SignalStrength.CompareTo(y.SignalStrength); | |
if (signalCompare != 0) | |
{ | |
return signalCompare; | |
} | |
var lastConnectedCompare = Comparer<DateTime?>.Default.Compare(x.LastConnected, y.LastConnected); | |
if (lastConnectedCompare != 0) | |
{ | |
return lastConnectedCompare; | |
} | |
return string.CompareOrdinal(x.FriendlyName, y.FriendlyName); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment