I'm trying to use the following code as a safe way to call an async service from within the constructor of my viewmodel as suggested in this post. Problem is, nothing from within the body of the this.WhenActivated ever fires, any ideas why?
here is my code:
class MainViewModel : ReactiveObject, ISupportsActivation, IMainViewModel
{
private IDataService _dataService;
private Part _part;
public Part MyPart
{
get { return _part; }
set { this.RaiseAndSetIfChanged(ref _part, value); }
}
public MainViewModel(IDataService dataService)
{
_dataService = dataService;
this.WhenActivated(disposables =>
{
_dataService.GetPart("9176900515")
.ToObservable()
.Subscribe(
result => { MyPart = result; },
exception => { LogMe.Log<string>(exception.Message); }
)
.DisposeWith(disposables);
});
}
private readonly ViewModelActivator activator = new ViewModelActivator();
ViewModelActivator ISupportsActivation.Activator
{
get { return activator; }
}
}
To make WhenActivated work inside a view model, the view model has to be the ViewModel of a view that implements IViewFor<MainViewModel>.
MainViewModel's WhenActivated will then be called by the view's WhenActvated.
Update:
This is done in WPF, but it's supported on all platforms (WPF, UWP, Xamarin).
The view implements IViewFor<TViewModel>. By best practices the ViewModel property is a DependencyProperty (or BindableProperty in Xam.Forms).
public partial class MainWindow : Window, IViewFor<MainViewModel>
{
public MainWindow()
{
InitializeComponent();
this.WhenActivated(d =>
{
// This will be called
});
}
public MainViewModel ViewModel
{
get => (MainViewModel)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(MainViewModel), typeof(MainWindow), new PropertyMetadata(null));
object IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = value as MainViewModel;
}
}
The WhenActivated in the VM will now be called when WhenActivated is called in the view.
class MainViewModel : ReactiveObject, ISupportsActivation
{
public ViewModelActivator Activator => _activator;
private ViewModelActivator _activator = new ViewModelActivator();
public MainViewModel()
{
this.WhenActivated(d =>
{
// This will be called
});
}
}
Implementing the ViewModel
For one, the WhenActivated pattern is described here: https://reactiveui.net/docs/handbook/when-activated/
WhenActivated is a way to track disposables. Besides that, it can be used to defer the setup of a ViewModel until it's truly required. WhenActivated also gives us an ability to start or stop reacting to hot observables, like a background task that periodically pings a network endpoint or an observable updating users current location. Moreover, one can use WhenActivated to trigger startup logic when the ViewModel comes on stage.
That part of the pattern seems already done right in the question.
Implementing the View
For your ViewModel to be activated, you have to fully follow a specific pattern which is provided in https://reactiveui.net/docs/getting-started/#create-views
Here are the key areas:
public partial class MainWindow : IViewFor<AppViewModel>
{
// Using a DependencyProperty as the backing store for ViewModel.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register("ViewModel",
typeof(AppViewModel), typeof(MainWindow),
new PropertyMetadata(null));
public MainWindow()
{
InitializeComponent();
ViewModel = new AppViewModel();
// ....
}
// ....
// Our main view model instance.
public AppViewModel ViewModel
{
get => (AppViewModel)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
// This is required by the interface IViewFor, you always just set it to use the
// main ViewModel property. Note on XAML based platforms we have a control called
// ReactiveUserControl that abstracts this.
object IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (AppViewModel)value;
}
}
Notice that the framework will notice the link from that specific MainWindow instance to that specific AppViewModel instance when the ViewModel setter is called:
ViewModel = new AppViewModel();
Related
Hi I want a task to run immediately after the view is opened without need for ViewModel class
This is how I do it right now
public class ContributorsViewModel : DemoViewModelBase<Model>
{
public ContributorsViewModel()
{
Task.Run(() => DataList = new DataService().GetDataList());
}
}
DemoViewModelBase:
public class DemoViewModelBase<T> : BindableBase
{
private IList<T> _dataList;
public IList<T> DataList
{
get => _dataList;
set => SetProperty(ref _dataList, value);
}
}
Bootstrapper:
containerRegistry.RegisterForNavigation<ContributorsView>();
I do not know how to do this without a viewmodel
You can do anything you want in code-behind, for example subscribe to the Loaded event:
internal class ContributorsView
{
public ContributorsView()
{
InitializeComponents();
Loaded += async (s, e) => await Task.Run( ...whatever... );
}
}
But keep in mind:
you won't be able to test this
you do not get the benefit of injected dependencies
without [...] ViewModel class
This seems to be a really bad idea(*), because what you want to do is essentially what view models are there for.
(*) unless you have some architectural that you didn't reveal
I am making a Universal winrt app using mvvm light. In ViewModelLocator I've registered my view in the builtin NavigationService of mvvm light
SimpleIoc.Default.Register<INavigationService>(() =>
{
var navigationService = new NavigationService();
navigationService.Configure("PreRegisterPage", typeof(PreRegisterPage));
return navigationService;
});
But when I try to navigate to that page using this code,
_navigationService.NavigateTo("PreRegisterPage");
It throws this exception
No such page: PreRegisterPage. Did you forget to call
NavigationService.Configure? Parameter name: pageKey
Am I missing something?
You probably forgot to pass an INavigationService object in the ViewModel ctor, here how your ViewModel should looks like:
public class MainViewModel : ViewModelBase
{
private INavigationService _navigationService;
private RelayCommand _navigateCommand;
public RelayCommand NavigateCommand
{
get
{
return _navigateCommand
?? (_navigateCommand = new RelayCommand(
() =>
{
_navigationService.NavigateTo("PreRegisterPage");
}));
}
}
public MainViewModel(INavigationService navigationService)
{
_navigationService = navigationService;
}
}
In the constructor of the PageService object, you have to call the Configure method with the ViewModel and the Page classes. As in the following:
Configure<MainViewModel, MainPage>();
The code for displaying the error message above is also in PageService.cs. I think it's a stupid way of doing it.
I use MVVM and I have to create a ViewModel class that should load lots of data when the View is opened.
Basically, when I create the viewmodel, it should use the database and get the data.
I used this approach first:
public class MainViewModel
{
public MainViewModel()
{
//set some properties
Title = "Main view";
//collect the data
StartLongOperation();
}
private void StartLongOperation()
{
Thread t=new Thread(...);
t.start();
}
}
It works and loads the data without blocking the UI thread.
Later I found this guideline about how to use constructor, and it does not recommend starting long operation from constructor.
√ DO minimal work in the constructor.
Constructors should not do much work other than capture the
constructor parameters. The cost of any other processing should be
delayed until required.
In my case, the data is required on opening the view.
My first idea was to use an event.
How should I avoid calling the long operation from construcTor? What is the best practice?
Miguel Castro has talked about a solution to this in one of his great Pluralsight courses. He binds to a property in the viewmodel called ViewLoaded which will obviously get bound when the view loads, this in turn will call your long running method.
So this goes in the view (Or a base class for all views to help with re-use):
public ViewBase()
{
// Programmatically bind the view-model's ViewLoaded property to the view's ViewLoaded property.
BindingOperations.SetBinding(this, ViewLoadedProperty, new Binding("ViewLoaded"));
}
public static readonly DependencyProperty ViewLoadedProperty =
DependencyProperty.Register("ViewLoaded", typeof(object), typeof(UserControlViewBase),
new PropertyMetadata(null));
And this is the ViewModel base class code:
public object ViewLoaded
{
get
{
OnViewLoaded();
return null;
}
}
protected virtual void OnViewLoaded() { }
Simply override the OnViewLoaded() method in your ViewModel and call the long running method from there.
Maybe use a factory pattern to avoid having the MainViewModel around but not populated.
public class VMFactory
{
public async Task<MainViewModel> GetVM()
{
MainViewModel vm = new MainViewModel();
await vm.LongOperation();
return vm;
}
}
public class MainViewModel
{
public MainViewModel()
{
//set some properties
Title = "Main view";
}
public async Task LongOperation()
{
(...)
}
}
or perhapse better. move the long running method out of the MainViewModel to a repository or service
public class VMRepository
{
public async Task LongOperation()
{
(...)
}
public async Task<MainViewModel> GetVM()
{
MainViewModel vm = new MainViewModel();
vm.DataWhichTakesAlongTime = await LongOperation();
return vm;
}
}
public class MainViewModel
{
public MainViewModel()
{
//set some properties
Title = "Main view";
}
public object DataWhichTakesAlongTime { get; set; }
}
To be honest though it sounds from the conversations around this question that you are simply using the constructor as a convenient trigger for a 'LoadDataNow' command and really you should add an ICommand, bind it to something in the view (Loaded) add loading spinners and completed events etc etc
Controversially I might also suggest you add a Controller Class to instantiate the repository view and vm and call the 'LoadData' method on the view. Not very MVVM I know but essentially doing the same stuff your IoC container does without having to jump through the hoops of configuration
Avoiding calling it is simple, just split it into 2 methods; The constructor and a GetData method you call when you open the view or after you set the data context.
The why is just about managing expectation. If you hadn't written the code and were writing a new view for someone else's view model, would you expect the constructor to start accessing a database? or would you expect it just to construct a view model and you need to make a second call to initiate getting the data?
Use your view lifecycle to execute this method. You can use Tasks to simplify the execution and you can bind to other properties to show progress. Example shown using a Windows Store App view.
ViewModel:
public class MainViewModel
{
public MainViewModel()
{
this.Title = "Main view";
}
public async Task StartLongOperationAsync()
{
this.IsLoading = true;
await Task.Run(() =>
{
//do work here
});
this.IsLoading = false;
}
}
And on the View:
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
await ((MainViewModel)this.DataContext).StartLongOperationAsync();
}
I don't know maybe it's wrong but sometimes i make(if i need retrun parameters)
public class MainViewModel
{
public MainViewModel()
{
//set some properties
Title = "Main view";
}
public static string GetMainViewModelString()
{
var mainViewModel = new MainViewModel();
return mainViewModel.GetString();
}
public string GetString()
{
/*your code*/
}
}
and then call
var stringData = MainViewModel.GetMainViewModelString();
but when it need i call some operation from constructor
I have a ListBox on a Window. This Window's model uses Caliburn with a conductor:
public class ShellViewModel : Conductor<IScreen>.Collection.OneActive
My screens are user controls (each UC has a Model) that gets loaded when I click a tab on TabControl.
I want to be able to access the ListBox selected item on all my screens.
How do I do that?
I can use EventAggreagator
In my AppBootstraper class I can add:
private CompositionContainer _container;
And under the Config method:
var batch = new CompositionBatch();
batch.AddExportedValue<IWindowManager>(new WindowManager());
batch.AddExportedValue<IEventAggregator>(new EventAggregator());
batch.AddExportedValue(_container);
_container.Compose(batch);
In my main ViewModel's constructor:
IEventAggregator eventAggregator
And in the bound property:
public Thing SelectedThing
{
get { return _selectedThing; }
set
{
_selectedThing = value;
NotifyOfPropertyChange(() => SelectedThing);
_eventAggregator.PublishOnUIThread(SelectedThing);
}
}
Then on my screen models:
public class MyScreenViewModel : Screen, IHandle<Thing>
In the constructor:
IEventAggregator eventAggregator
Then:
_eventAggregator = eventAggregator;
_eventAggregator.Subscribe(this);
Interface implementation:
void IHandle<Thing>.Handle(Thing selectedThing)
{
this.SelectedThing = selectedThing;
}
More about it: Caliburn Micro Part 4: The Event Aggregator
The other option (although more coupled) is to use the fact that Screens have a "Parent" property which you can use to access their conductor; so you could do something like the following in the MyScreenViewModel.
void GetSelectedThing()
{
var conductingVM= this.Parent as ShellViewModel ;
this.SelectedThing = conductingVM.SelectedThing;
}
I build up a custom TreeView class, with settings for each node such as "name/background" etc. I also have a ICommand property that can be set so that each node can have a custom method executed if necessary.
I build all of this in a "treeview service class", which then sends the menu to the usercontrol via MVVMLight Messenger. This all works just fine, but my problem is that if i dont specify a custom command for the node, i want it to execute a "default action", which should be located in the viewmodel that recieves the message from the Messenger service.
Basically my question is: How do i expose a RelayCommand in the MainViewModel , so that i can reference it from another viewmodel (or my service class), when building my tree.
To reference ViewModel A in ViewModel B you can use MVVMLight´s ViewModelLocator like in the Template samples:
Your ViewModelLocator class:
public class ViewModelLocator
{
static ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
// Register your services
//...
// Register your ViewModels
SimpleIoc.Default.Register<MainViewModel>();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance",
"CA1822:MarkMembersAsStatic",
Justification = "This non-static member is needed for data binding purposes.")]
public MainViewModel Main
{
get
{
return ServiceLocator.Current.GetInstance<MainViewModel>();
}
}
}
and in your NodeViewModel you could access your default command for example like this:
public class NodeViewModel : ViewModelBase
{
private ViewModelLocator locator = new ViewModelLocator();
public RelayCommand NodeCommand
{
get
{
return locator.Main.DefaultCommand;
}
}
}
You can find a full small sample when you create a MVVM Light project by using the MVVM Light visual studio templates.
hope this helps!
I believe RelayCommand is an ICommand. You can just expose it as a property on the viewmodel:
public ICommand MyCommand { get; set;}