Skip to content

Nullability Problems with Bind() calls #2467

@Noggog

Description

@Noggog

Describe the bug

Existing instructions on how to set up XAML code behind bindings have a few usability issues due to the new nullability compiler features being added.

Steps To Reproduce

Let's take a 101 binding example inspired from the RxUI docs (https://www.reactiveui.net/docs/handbook/data-binding/windows-presentation-foundation):

// Setup run button
this.OneWayBind(this.ViewModel, vm => vm.RunAllCommand, view => view.RunButton.Command)
    .DisposeWith(disposable);

This should do the very simple job of binding a button's command to a viewmodel's command property.

Two problems come up with nullability.

  1. OneWayBind's viewModel parameter is not null, while ReactiveUserControl's ViewModel member is. This means the user has to add the ! operator to get things to pass in, which is undesirable:
// Setup run button
this.OneWayBind(this.ViewModel!, vm => vm.RunAllCommand, view => view.RunButton.Command)
    .DisposeWith(disposable);

It's my understanding that ! should be avoided whenever possible, as it's bad coding habit that starts reintroducing routes for null ref errors into your code again.

  1. OneWayBind returns a IReactiveBinding object that is nullable. This doesn't hook into DisposeWith cleanly, as it expects a non null item. So now the user has to add ? checks after every bind call, which is not too clean:
// Setup run button
this.OneWayBind(this.ViewModel!, vm => vm.RunAllCommand, view => view.RunButton.Command)
    ?.DisposeWith(disposable);

Since bindings like this are written -all- the time when developing a GUI, I think it would be good to set up the structure to not require any ! usage, for sure, and then potentially try to avoid the need for elvis operators, if possible.

Expected behavior

I would expect this code without any extra operators to function:

// Setup run button
this.OneWayBind(this.ViewModel, vm => vm.RunAllCommand, view => view.RunButton.Command)
    .DisposeWith(disposable);

For me, my suggested solutions would be to make the viewModel parameter on OneWayBind nullable, so that it can take ReactiveControl's ViewModel property directly and cleanly. Since the bind definitions return potentially null bindings already, just have a null viewmodel short circuit and return a null binding, perhaps? Seems like the cleanest route.

public IReactiveBinding<TView, TViewModel, TVProp> OneWayBind<TViewModel, TView, TVMProp, TVProp>(
        TViewModel? viewModel,
        TView view,
   ...

Second, since the binding calls are returning potentially null objects, it might be nice to upgrade DisposeWith to take nullable items, to remove the need for the ? call between the binding and the dispose every time.

[return: NotNullIfNotNull("item")]
public static T? DisposeWith<T>(this T? item, CompositeDisposable compositeDisposable)
    where T : class, IDisposable
{
    if (compositeDisposable == null)
    {
        throw new ArgumentNullException(nameof(compositeDisposable));
    }
    
    if (item == null) return item;

    compositeDisposable.Add(item);
    return item;
}

Perhaps the new class constraint isn't desirable and can be avoided, but this general idea is what I'm thinking.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions