所以我正在开发一个可以调整(除其他外)设备音量的应用程序。所以我开始使用的是一个非常简单的模型,它实现了INotifyPropertyChanged。就我所知,在这样一个简单的场景中不需要ViewModel。设置卷属性时调用INPC,模型生成TCP消息以告知设备更改卷。
然而,这是它变得复杂的地方。该应用不必更改音量,也可以直接在设备上更改音量,甚至可以通过应用程序的其他手机进行更改。从设备获取这些更改的唯一方法是定期轮询它。
所以我认为合理的是改变结构。所以现在我有一个代表实际设备的DeviceModel。我添加了一个VolumeViewModel。 DeviceModel类现在处理生成TCP消息。它还会定期轮询设备。但是,假设DeviceModel发现卷已更改。它应该如何传播回VolumeViewModel,以便所有更改都是来自UI和实际设备的双向?如果我把INPC放在DeviceModel中,似乎我的VolumeViewModel变得多余了。也许对于这个简单的人为设计的例子来说很好,但是让我们说这个设备比1卷更复杂。我在想VM可以包含对Model的引用,而volume属性可能只是对DeviceModel中卷的引用,但它仍然没有真正解决我的问题。
如果DeviceModel卷发生更改,则引用不会更改,因此在我看来,这不会触发VolumeViewModel中volume属性的setter函数。当轮询看到不同的卷时,我是否让ViewModel将一个事件处理程序注入要调用的Model中?我在两者中都使用INPC(实现它的方式是什么样的?)
答案 0 :(得分:2)
设定方向很明确。你想明确地得到它。所以我们需要像
这样的东西class MyDeviceService : IDeviceService
{
public async Task SetVolumeAsync(int volume) { }
public async Task<int> GetVolumeAsync() { }
}
// ViewModel
class DeviceViewModel : INotifyPropertyChanged
{
public int Volume { get{ ... } set { ... } }
public DeviceViewModel(IDeviceService service) { ... }
}
对于更新,您有不同的选择:
<强>临强>
<强>缺点:强>
class MyDeviceService
{
public Action<int> VolumeChangedCallback { get; set; }
public async Task SetVolumeAsync(int volume) { }
public async Task<int> GetVolumeAsync() { }
// producer
VolumeChangedCallback(newVolume);
}
// consumer
myDeviceService.VolumeChangedCallback = v => Volume = v;
// deregistration
myDeviceService.VolumeChangedCallback = null;
<强>临强>
<强>缺点:强>
class MyDeviceService
{
public event EventHandler<VolumeChangedEventArgs> VolumeChanged;
public async Task SetVolumeAsync(int volume) { }
public async Task<int> GetVolumeAsync() { }
// producer
VolumeChanged(new VolumeChangedEventArgs(newVolume));
}
// consumer
MessagingCenter.Subscribe<MyDeviceService, int>(this,
MyDeviceService.VolumeMessageKey, newVolume => Volume = newVolume);
// needs deregistration
MessagingCenter.Unsubscribe<MyDeviceService, int>(this,
MyDeviceService.VolumeMessageKey, newVolume => Volume = newVolume);
<强>临强>
<强>缺点:强>
class MyDeviceService
{
public static string VolumeMessageKey = "Volume";
public async Task SetVolumeAsync(int volume) { }
public async Task<int> GetVolumeAsync() { }
// producer
MessagingCenter.Send<MyDeviceService, int>(this,
VolumeMessageKey, newVolume);
}
// consumer
MessagingCenter.Subscribe<MyDeviceService, int>(this,
MyDeviceService.VolumeMessageKey, newVolume => Volume = newVolume);
// needs deregistration
MessagingCenter.Unsubscribe<MyDeviceService, int>(this,
MyDeviceService.VolumeMessageKey, newVolume => Volume = newVolume);
如果你有事件流,使用Reactive extensions总是很好。
<强>临强>
IEnumerable
(例如Where(volume => volume > 10)
)<强>缺点:强>
class MyDeviceService
{
IObservable<int> VolumeUpdates { get; }
public async Task SetVolumeAsync(int volume) { }
public async Task<int> GetVolumeAsync() { }
}
// consumer
_volumeSubscription = myDeviceService.VolumeUpdates
.Subscribe(newVolume => Volume = newVolume);
// deregistration
// - implicitly, if object gets thrown away (but not deterministic because of GC)
// - explicitly:
_volumeSubscription.Dispose();
我在模型中遗漏了INPC,因为那个事件更糟糕,因为你必须比较属性名称。 如果您查看这些示例,您会发现它们只是在您订阅和取消订阅的方式上有所不同。主要区别在于它们提供的灵活性。就个人而言,我会选择Reactive Extensions;)但事件和消息也很好。因此,请选择您和您的团队成员最了解的方法。你只需要记住:
答案 1 :(得分:1)
我假设您打算向用户显示显示当前音量的UI(例如滑块小部件)。因此,您真正面临的挑战是,无法立即确认操纵滑块的任何尝试 - 设备可能需要一些时间才能响应,一旦它完成,它甚至可能不接受请求(或者可能被本地操作覆盖) 。然而,您仍然需要向移动应用程序用户显示他们的请求正在被处理 - 否则他们会认为它正在发生故障。
我也必须在应用程序中解决这个问题 - 虽然我的例子是一个更复杂的情况。我的应用程序用于控制灌溉管理硬件的大型安装,具有许多设备(具有不同版本的固件和不同程度的远程控制功能)。但最终问题是一样的。我用标准MVVM解决了它。
对于每个设备,创建一个跟踪两个不同值的viewmodel:硬件的实际上次已知(已报告)状态,以及应用最近可能请求的任何“待处理”值。通过标准INPC绑定将可视控件绑定到“挂起”值。在这些值的设置器中,如果新值与上次已知的硬件状态不同,则它将触发对设备的异步请求以转换到所需的状态。在剩下的时间里,您只需使用任何对您有意义的机制轮询设备状态(推送通知可能会更好,但在我的情况下,我使用的基础架构只能支持活动轮询)。您将使用新硬件状态值以及挂起值进行更新(除非已有待处理的其他值)。
在应用程序UI中,您可能希望显示实际的硬件状态值以及允许用户操作的“待定”值。对于滑块,您可能希望实现反映报告的硬件值(只读)的“ghost”滑块。对于交换机,您可能希望禁用它们,直到硬件报告与挂起值相同的值。无论您的应用程序的设计语言有什么意义。
这留下了如何处理硬件不(或不能)尊重请求的情况的最终边缘情况。当设备只能达到10时,用户可能会尝试将音量调高到11.或者有人按下设备上的物理按钮来静音。或者也许其他人正在运行移动应用程序并与您争夺控制权。无论如何,通过为待处理的操作建立最大等待超时可以很容易地解决它。例如,假定10秒后未满足的任何卷更改请求被抢占,并且UI将通过设置挂起值=上次报告的值来停止等待它。
无论如何,祝你好运!处理得很好,但值得努力!