0

I am using ReactiveUI 7.4 in an application I'm working on. One of the views is a master/detail view. When a new master record is selected, I want to load detail items from the database. This works as expected using the WhenAnyValue observable to set the value of an ObservableAsPropertyHelper in my sample code below.

Now, I have a ReactiveCommand used to save data. This command calls a stored procedure that does some processing and updates to child items related to the currently selected master item. Because the related items may change on the database side, I would like to trigger the related items to be reloaded using the logic that I've already set up in the WhenAnyValue observable.

Below is a very simplified example that reproduces what I'm trying to do. I tried calling RaisePropertyChanged at the end of the save, but it doesn't trigger the WhenAnyValue observable. I can set the selected item to null and then reset it back to the original value, but that feels wrong here.

Is there a better way to handle this situation, or another way to trigger the WhenAnyValue/ObservableAsPropertyHelper?

public class SampleViewModel : ReactiveObject
{
    public SampleViewModel()
    {
        // log any changes (we can see the RaisePropertyChanged triggers this observable, but not the one above).
        this.Changed.Skip(1).Subscribe(x => Console.WriteLine($"\tChanged = {x.PropertyName}"));

        // when the selected item changes, we want to load related items from the database
        _relatedItems = this.WhenAnyValue(x => x.SelectedItem)
            .Where(row => row != null)
            .Select(row =>
            {
                // get related data from the cache or database...
                Console.WriteLine($"\tGetting Related Items for {row["Value"]}");
                return new DataTable();
            })
            .ToProperty(this, x => x.RelatedItems);

        Save = ReactiveCommand.Create(() =>
        {
            // Save the selected item. Stored procedure does some extra processing of related items

            // Now, we want to trigger a reload of the related items.
            this.RaisePropertyChanged(nameof(SelectedItem));

            /* NOTE: The following works, but feels wrong.
            var oldSelection = SelectedItem;
            SelectedItem = null;
            SelectedItem = oldSelection;
            */
        });
    }

    public ReactiveCommand Save { get; }

    private readonly ObservableAsPropertyHelper<DataTable> _relatedItems;
    public DataTable RelatedItems => _relatedItems.Value;

    private DataRow _selectedItem;
    public DataRow SelectedItem
    {
        get { return _selectedItem; }
        set { this.RaiseAndSetIfChanged(ref _selectedItem, value); }
    }
}

internal class Program
{
    private static void Main(string[] args)
    {
        using (var table = new DataTable())
        {
            table.Columns.Add(new DataColumn("Value", typeof(string)));
            table.Rows.Add(new[] { "First Row" });
            table.Rows.Add(new[] { "Second Row" });

            var instance = new SampleViewModel();
            Console.WriteLine("Selecting First Row");
            instance.SelectedItem = table.Rows[0];

            Console.WriteLine();
            Console.WriteLine("Saving Data");
            Observable.Start(() => { }).InvokeCommand(instance.Save);

            Console.WriteLine();
            Console.WriteLine("Selecting Second Row");
            instance.SelectedItem = table.Rows[1];

            Console.WriteLine();
        }
    }
}

Outputs

Selecting First Row
    Changed = SelectedItem
    Getting Related Items for First Row
    Changed = RelatedItems

Saving Data
    Changed = SelectedItem
    // should be getting related items here...

Selecting Second Row
    Changed = SelectedItem
    Getting Related Items for Second Row
    Changed = RelatedItems

Edit (working code)

Lee McPherson's Answer pointed me in the right direction. The constructor has been updated to the following:

public SampleViewModel()
{
    // log any changes
    this.Changed.Skip(1).Subscribe(x => Console.WriteLine($"\tChanged = {x.PropertyName}"));

    Save = ReactiveCommand.Create(() =>
    {
        // Save the selected item. Stored procedure does some extra processing of related items
    });

    // when the save command finishes execution, get the currently selected "master" record
    var saveCommandDone = Save.IsExecuting
        .Where(executing => !executing).Skip(1)
        .Do(_ => Console.WriteLine("\tCommand finished execution"))
        .Select(_ => SelectedItem);

    // when the master record changes, or data is saved, reload the related items
    _relatedItems = Observable.Merge(saveCommandDone, this.WhenAnyValue(x => x.SelectedItem))
        .Where(row => row != null)
        .Select(row =>
        {
            // get related data from the cache or database...
            Console.WriteLine($"\tGetting Related Items for {row["Value"]}");
            return new DataTable();
        })
        .ToProperty(this, x => x.RelatedItems);
}
Selecting First Row
    Changed = SelectedItem
    Getting Related Items for First Row
    Changed = RelatedItems

Saving Data
    Command finished execution
    Getting Related Items for First Row
    Changed = RelatedItems

Selecting Second Row
    Changed = SelectedItem
    Getting Related Items for Second Row
    Changed = RelatedItems

Another possible answer: I also found that I could convert the "load related items" logic into a ReactiveCommand and use InvokeCommand to accomplish the same thing.

var loadRelatedData = ReactiveCommand.Create<DataRow, DataTable>(row => new DataTable());
_relatedItems = loadRelatedData.ToProperty(this, x => x.RelatedItems);

// when the selected item changes, trigger the command used to load related data
this.WhenAnyValue(x => x.SelectedItem)
    .Where(row => row != null)
    .InvokeCommand(loadRelatedData);

Save = ReactiveCommand.Create(() =>
{
    // Save the data and trigger a reload of the related data.
    Observable.Return(SelectedItem).InvokeCommand(loadRelatedData);
});
Eugene Pawlik
  • 1,170
  • 1
  • 15
  • 21

1 Answers1

0

Your Save ReactiveCommand has an IsExecuting IObservable. You can try Observable.Merge to combine the observable you create with WhenAnyValue with the command invoking.

Observable.Merge(this.Save.IsExecuting, _relatedItems).Subscribe(x=>{
    //do stuff
});

Here's a related answer: How do I merge several observables using WhenAny(...) in ReactiveUI?

Now that I think about it, I don't know if the ObservableAsPropertyHelper will work. But if it doesn't, you can just create the Observable as a separate item before converting it with ToProperty(...).

Lee McPherson
  • 931
  • 6
  • 20