简体   繁体   English

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

[英]Looking for advice on how to implement a “volume” property in MVVM C# Xamarin

So I am developing an app that can adjust (amongst other things) the volume of a device. 因此,我正在开发一个可以调整设备音量的应用程序。 So what I started with was a very simple Model which implements INotifyPropertyChanged. 因此,我从一个非常简单的模型开始,该模型实现了INotifyPropertyChanged。 There is no need for a ViewModel in such a simple scenario as far as I can tell. 据我所知,在这样一个简单的场景中就不需要ViewModel了。 INPC is called when the volume property is set, and the Model generates a TCP message to tell the device to change the volume. 设置音量属性后,将调用INPC,并且模型会生成一条TCP消息以告知设备更改音量。

However, this is where it gets complicated. 但是,这变得很复杂。 The volume does not have to be changed by the app, it could also be changed directly on the device, or even by another phone with the app. 音量不必由应用程序更改,也可以直接在设备上更改,甚至可以由带有该应用程序的另一部手机更改。 The only way to get these changes from the device is to poll it periodically. 从设备获取这些更改的唯一方法是定期轮询它。

So what I think is reasonable is to change the structure a bit. 所以我认为合理的是稍微改变一下结构。 So now I have a DeviceModel which represents the actual device. 所以现在我有了一个表示实际设备的DeviceModel。 I add a VolumeViewModel. 我添加一个VolumeViewModel。 The DeviceModel class now handles generating the TCP messages. DeviceModel类现在可以处理生成TCP消息。 It also periodically polls the device. 它还会定期轮询设备。 However, lets say the DeviceModel finds that the volume changed. 但是,可以说DeviceModel发现卷已更改。 How should this propagate back to the VolumeViewModel such that all changes are two-way both from the UI, and from the actual device? 这应该如何传播回VolumeViewModel,以使所有更改都可以从UI和实际设备进行双向转换? If I put INPC in the DeviceModel, it seems my VolumeViewModel becomes superfluous. 如果将INPC放在DeviceModel中,似乎我的VolumeViewModel变得多余了。 Perhaps for this simple contrived example that's fine, but lets say the device is more complicated than just 1 volume. 也许对于这个简单的人为的例子来说还不错,但是可以说该设备比1个体积还要复杂。 I was thinking the VM could contain a reference to the Model, and the volume property could just be a reference to the volume in the DeviceModel but it still doesn't really solve my problem. 我以为VM可以包含对Model的引用,而volume属性可以只是对DeviceModel中的卷的引用,但它仍然不能真正解决我的问题。

If the DeviceModel volume changes, the reference isn't changing, so it seems to me this would not trigger the setter function for the volume property in the VolumeViewModel. 如果DeviceModel卷更改,则引用不会更改,因此在我看来,这不会触发VolumeViewModel中volume属性的setter函数。 Do I have the ViewModel inject an event handler into the Model to be called when polling sees a different volume? 当轮询看到不同的体积时,是否让ViewModel将事件处理程序注入到要调用的模型中? Do I use INPC in both (what would implementing it that way look like?) 我是否都使用INPC(以这种方式实现它会是什么样?)

Set direction is clear. 设定方向清晰。 And you want to get it explicitly. 而您想明确地获取它。 So we need something like 所以我们需要像

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) { ... }
}

For the update you have different options: 对于更新,您有不同的选择:

Callback 打回来

Pro: 优点:

  • Easy to implement 易于实施

Con: 缺点:

  • only one subscriber 只有一个订户
  • looks like a bad implementation of events (in our scenario) 看起来是事件的错误实现(在我们的场景中)
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;

Event 事件

Pro: 优点:

  • Language feature (built in) 语言功能(内置)
  • Multiple subscribers 多个订户

Con: 缺点:

  • ??? ???
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);

Messaging 讯息传递

Pro: 优点:

  • Easy Sub / Unsub 简易订阅/取消订阅
  • Multiple subscribers 多个订户
  • Multiple senders 多个发件人
  • Receiver does not need to know the sender 接收者不需要知道发送者

Con: 缺点:

  • external library needed (but included in Xamarin.Forms , MvvMCross, other MvvM Frameworks) 需要外部库(但包含在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);

Observable 可观察的

Using Reactive extensions is always nice, if you have event streams. 如果您有事件流,那么使用响应式扩展总是很不错的。

Pro: 优点:

  • Easy Sub / Unsub 简易订阅/取消订阅
  • Multiple subscribers 多个订户
  • Filterable like IEnumerable (eg Where(volume => volume > 10) ) IEnumerable一样可过滤(例如Where(volume => volume > 10)

Con: 缺点:

  • external library just for one case 外部库仅用于一种情况
  • high learning effort due totally new approach 全新的方法使学习努力很高
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();

Conclusion 结论

I left out INPC in the model, because that's events but worse, because you have to compare the property names. 我在模型中省略了INPC,因为那是事件,但更糟糕的是,因为您必须比较属性名称。 If you have a look at these examples, you see, that they just differ in the way you subscribe and unsubscribe. 如果查看这些示例,就会发现它们只是在订阅和退订方式上有所不同。 The main difference is the flexibility they offer. 主要区别在于它们提供的灵活性。 Personally, I'd go for Reactive Extensions ;) But Events and Messaging are fine, too. 就个人而言,我会选择响应式扩展;)但是事件和消息传递也可以。 So go for the approach that you and your team members understand the best. 因此,请采用您和您的团队成员最了解的方法。 You just have to remember: 您只需要记住:

ALWAYS deregister! 总是注销! ^^ ^^

I am presuming that you intend to show a UI to the user that displays the current volume (such as a slider widget). 我假设您打算向用户显示一个显示当前音量的UI(例如滑块小部件)。 Therefore your real challenge is the fact that any attempts to manipulate that slider cannot be immediately confirmed - it may take some time for the device to respond, and once it does it may not even accept the request (or might be overridden by local manipulation). 因此,您面临的真正挑战是无法立即确认任何操纵该滑块的尝试-设备可能需要一些时间才能做出响应,一旦响应,它甚至可能无法接受该请求(或可能被本地操纵所覆盖) 。 Yet you still have a need to show the mobile app user that their request is being processed - or else they will assume it is malfunctioning. 但是,您仍然需要向移动应用程序用户显示其请求正在处理中-否则他们会认为该请求有问题。

I've had to solve this in an app as well - although my example was a much more complicated situation. 我也必须在应用程序中解决此问题-尽管我的示例是一个更为复杂的情况。 My app is used to control large installations of irrigation management hardware, with many devices (with varying versions of firmware and varying degrees of remote control capabilities). 我的应用程序用于控制带有许多设备的灌溉管理硬件的大型安装(具有不同版本的固件和不同程度的远程控制功能)。 But ultimately the problem was the same. 但是最终问题还是一样。 I solved it with standard MVVM. 我用标准MVVM解决了它。

For each device, create a viewmodel that tracks two distinct values: the actual last known (reported) status of the hardware, and any "pending" value that may have been recently requested by the app. 对于每个设备,创建一个跟踪两个不同值的视图模型:硬件的实际最后一次已知(报告)状态,以及应用程序最近可能请求的任何“待定”值。 Bind the visual controls to the "pending" values via standard INPC bindings. 通过标准INPC绑定将可视控件绑定到“待处理”值。 In the setters for those values, if the new value differs from the last known hardware status, then it would trigger an async request to the device to transition to the desired status. 在这些值的设置程序中,如果新值不同于上一个已知的硬件状态,则它将触发对设备的异步请求,以转换为所需状态。 And for the rest of the time, you just poll the device status using whatever mechanism makes sense for you (push notifications might be better, but in my case the infrastructure I was working with could only support active polling). 在剩下的时间里,您仅使用对您有意义的任何机制来轮询设备状态(推送通知可能会更好,但是在我的情况下,我正在使用的基础结构只能支持主动轮询)。 You would update with the new hardware status values, and also the pending values (unless a different value was already pending). 您将使用新的硬件状态值和未决值进行更新(除非已经有其他值未决)。

In the app UI, you probably want to show the actual hardware status values as well as the "pending" values that the user is allowed to manipulate. 在应用程序用户界面中,您可能希望显示实际的硬件状态值以及允许用户操作的“待定”值。 For sliders, you might want to implement a "ghost" slider thumb that reflects the reported hardware value (read-only). 对于滑块,您可能需要实现一个“重影”滑块,以反映报告的硬件值(只读)。 For switches, you might want to disable them until the hardware reports the same value as the pending value. 对于开关,您可能要禁用它们,直到硬件报告与待定值相同的值为止。 Whatever makes sense for your app's design language. 对于您应用的设计语言而言,任何有意义的选择。

This leaves the final edge case of how to deal with situations where the hardware does not (or cannot) respect a request. 剩下的最后情况是如何处理硬件不(或不能)尊重请求的情况。 Perhaps the user tries to turn up the volume to 11 when the device can only go up to 10. Or maybe someone presses a physical pushbutton on the device to mute it. 也许用户尝试在设备只能调高到10时将音量调高到11。或者也许有人按下设备上的物理按钮将其静音。 Or maybe someone else is running the mobile app and fighting you for control of it. 也许其他人正在运行该移动应用程序,并争夺您的控制权。 In any event, it is easily solved by establishing a maximum wait timeout for pending manipulations. 无论如何,通过为未决操作建立最大等待超时可以轻松解决该问题。 For example, any volume change requests that aren't met after 10 seconds are assumed to be pre-empted and the UI would just stop waiting for it by setting the pending value = last reported value. 例如,假定10秒钟后未满足的任何音量更改请求都被抢占,并且UI会通过设置未决值=最后报告的值来停止等待。

Anyhow, good luck! 无论如何,祝你好运! It's a challenging thing to handle well, but worth the effort! 处理得好是一件具有挑战性的事情,但值得付出!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM