寻找有关如何在MVVM C#Xamarin中实现“卷”属性的建议

时间:2016-04-12 21:59:14

标签: c# mvvm xamarin

所以我正在开发一个可以调整(除其他外)设备音量的应用程序。所以我开始使用的是一个非常简单的模型,它实现了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(实现它的方式是什么样的?)

2 个答案:

答案 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);

<强>信息

<强>临

  • Easy Sub / Unsub
  • 多个订阅者
  • 多个发件人
  • 接收方无需了解发件人

<强>缺点:

  • 需要外部库(但包含在Xamarin.Forms,MvvMCross,其他MvvM框架中)
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总是很好。

<强>临

  • Easy Sub / Unsub
  • 多个订阅者
  • 可过滤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将通过设置挂起值=上次报告的值来停止等待它。

无论如何,祝你好运!处理得很好,但值得努力!