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);
});