Skip to content

Instantly share code, notes, and snippets.

@kentcb
Last active July 18, 2016 06:37
Show Gist options
  • Save kentcb/63c06262b2a36e21fa94873c7fff76dc to your computer and use it in GitHub Desktop.
Save kentcb/63c06262b2a36e21fa94873c7fff76dc to your computer and use it in GitHub Desktop.
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