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;
}
Related
I've got a behavior in my .net Core 3.1 WPF Application, which calls a command inside the ViewModel, after the view is displayed.
public class LoadedBehavior
{
public static DependencyProperty LoadedCommandProperty
= DependencyProperty.RegisterAttached(
"LoadedCommand",
typeof(ICommand),
typeof(LoadedBehavior),
new PropertyMetadata(null, OnLoadedCommandChanged));
private static void OnLoadedCommandChanged
(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
if (depObj is FrameworkElement frameworkElement && e.NewValue is ICommand)
{
frameworkElement.Loaded
+= (o, args) => { (e.NewValue as ICommand)?.Execute(null); };
}
}
public static ICommand GetLoadedCommand(DependencyObject depObj)
{
return (ICommand)depObj.GetValue(LoadedCommandProperty);
}
public static void SetLoadedCommand(
DependencyObject depObj,
ICommand value)
{
depObj.SetValue(LoadedCommandProperty, value);
}
}
This Behavior is attached inside the View:
behaviors:LoadedBehavior.LoadedCommand="{Binding LoadedCommand}"
I am working with Prisms RegionManager to inject my Views into specific areas inside the views. When I now try to inject a new view, the loaded command from the old view is called again. This seems like it comes from the bevavior.
For a better understanding, here is also the code which gets called to show a new view inside the specific region
public class NavigationService
{
private readonly IServiceLocator _serviceLocator;
private readonly IRegionManager _regionManager;
public NavigationService(IServiceLocator serviceLocator, IRegionManager regionManager)
{
_serviceLocator = serviceLocator;
_regionManager = regionManager;
}
public void Navigate(string regionName, object view)
{
RemoveAllViews(regionName);
_regionManager.AddToRegion(regionName, view);
}
public void Navigate<T>(string regionName) where T : FrameworkElement
{
var view = _serviceLocator.GetInstance<T>();
Navigate(regionName, view);
}
public void RemoveAllViews(string regionName)
{
_regionManager.Regions[regionName].RemoveAll();
}
}
Can anyone tell me, what I do wrong here? Or is this behavior not the way to go?
Edit
Right after posting this, I found the problem: The Loaded Command gets called multiple times. This seems to be caused by when the content of this view changes. So everytime I add a new view, the parent view calls it's loaded event. Is there a way to run the command only once the view is displayed?
The Loaded event is quite inreliable for triggering action with the intention of one time when the control is loaded. From the reference of the Loaded event for FrameworkElement.
Loaded and Unloaded might both be raised on controls as a result of user-initiated system theme changes. A theme change causes an invalidation of the control template and the contained visual tree, which in turn causes the entire control to unload and reload. Therefore Loaded cannot be assumed to occur only when a page is first loaded through navigation to the page.
In Prism you can act on navigation by creating a custom region behavior. In your example, you want to execute a command on a view model, once the view is added to a region. Create an interface that all your target view models implement with a command that should be executed when the view is displayed first.
public interface IInitializableViewModel
{
ICommand Initialize { get; }
}
Create a region behavior that watches the Views collection of a region and executes a command once, when a view is added to the region. It will check the data context of each view, if it implements the interface, the command is not null and command can execute.
public class InitializableDataContextRegionBehavior : RegionBehavior
{
public const string BehaviorKey = nameof(InitializableDataContextRegionBehavior);
protected override void OnAttach()
{
Region.Views.CollectionChanged += OnViewsCollectionChanged;
}
private void OnViewsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var frameworkElement in e.NewItems.OfType<FrameworkElement>())
{
if (frameworkElement.DataContext is IInitializableViewModel initializableViewModel &&
initializableViewModel.Initialize != null &&
initializableViewModel.Initialize.CanExecute(null))
{
initializableViewModel.Initialize.Execute(null);
}
}
}
}
}
Add the custom region behavior in your Prism application to the region behaviors collection.
protected override void ConfigureDefaultRegionBehaviors(IRegionBehaviorFactory regionBehaviors)
{
base.ConfigureDefaultRegionBehaviors(regionBehaviors);
regionBehaviors.AddIfMissing(InitializableDataContextRegionBehavior.BehaviorKey, typeof(InitializableDataContextRegionBehavior));
}
The command on each view model will execute exactly once, when the corresponding view is added to any region. Using an interface here was easier for demonstration purposes, but you can also create an attached property for your command that you attach to your view and bind to the view model.
I really made a search for this topic and did not find anything, and because of that, I am asking the question here.
I have a WPF application with Prism installed.
I have wired the view-model with the view automatically by name convention
<UserControl x:Class="Views.ViewA"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True">
and the model in the 'Model' like this
public class ViewAViewModel {
public ViewAViewModel () {
// time-resource consuming operations
}
}
the automatic binding work perfectly without a problem and the view and its corresponding view-model is matching, but the problem here.
I have a lot of those views say (50) and for every one of them, the view-model will be created with constructor exhausting the processes. This will make the startup of the application longer and also it will create a lot of view-models objects and put them in the RAM without being sure that they will be used at all.
What I need is to create the view-model class when the view is activated (I mean when the view is navigated to). Is this possible and if yes how?
Update
here is how I register the view with the Module, this is causing all the views to be created when the startup of the module.
public class Module1 : IModule
{
public void OnInitialized(IContainerProvider containerProvider)
{
var regionManager = containerProvider.Resolve<IRegionManager>();
regionManager.RegisterViewWithRegion("region1", typeof(View1));
regionManager.RegisterViewWithRegion("region1", typeof(View2));
is there any way to delay the creating of the views, until the navigation request come?
You could use navigation, for each view.
Or you must create an interfaces for your view and view model.
An example:
public interface IMyView
{
IMyViewModel ViewModel { get; set; }
}
public interface IMyViewModel
{
}
In the module or app.cs, in the method RegisterTypes you should register these.
containerRegistry.Register<IMyView, MyView>();
containerRegistry.Register<IMyViewModel, MyViewModel>();
You must implement IMyView interface in your MyView.cs class.
public partial class MyView : UserControl, IMyView
{
public MyView(IMyViewModel viewModel)
{
InitializeComponent();
ViewModel = viewModel;
}
public IMyViewModel ViewModel
{
get => DataContext as IMyViewModel;
set => DataContext = value;
}
}
After you could use it:
public void OnInitialized(IContainerProvider containerProvider)
{
var regionManager = containerProvider.Resolve<IRegionManager>();
var firstView = containerProvider.Resolve<IMyView>();
regionManager.AddToRegion(RegionNames.MainRegion, firstView);
}
In such case you shouldn't use ViewModelLocator.AutoWireViewModel in your view.
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();
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;}
[Export]
public sealed class MainViewModel : NotificationObject
{
[Import]
public ISomeService MyService { get; private set; }
...
}
In order to INJECT this class as the DataContext to my View, I have to mark it as Export so MEF creates an instance of it in the Catalog. The problem is that the main window needs to create other windows and pass in orders, I'm not sure how to go about that without breaking the MVVM approach.
I figure that an ICommand will trigger something on my MainViewModel to generate a new ViewModel, but then after that happens I can't really force a new Window (view) to open up from the ViewModel. Plus, I can't even really create a new ViewModel from my MainViewModel because then MEF won't really work, right?
[Export]
public sealed class MainViewModel : NotificationObject
{
[Import]
public ISomeService MyService { get; private set; }
private ObservableCollection<IOrderViewModel> Orders { get; set; }
public void OpenOrder(int id)
{
//Pseudo-code to ensure that duplicate orders are not opened)
//Else create/open the new order
var order = new OrderViewModel(id);
OpenOrders.Add(order);
}
}
2 problems here:
Since I "newed" the OrderViewModel services are not autoloaded via MEF.
How does this code on my ViewModel layer (appropriate layer) create the necessary view as a NEW WINDOW (child of the main window), and then link this new OrderViewModel as the DataContext?
The way to avoid 'new-ing' the OrderViewModel is to use a factory:
[Export]
public class OrderViewModelFactory
{
[Import]
public ISomeDependency ImportedDependency { get; set; }
public OrderViewModel Create(int id)
{
return new OrderViewModel(id, this.ImportedDependency);
}
}
Then import the factory into your MainViewModel as a dependency and MEF will take care of filling everything in as required.
To get around the problem of instantiating windows, we have created a DialogService that does something like:
[Export]
public class DialogService
{
public bool? ShowDialog(string regionName, object data = null)
{
var window = new Window();
var presenter = new ContentControl();
presenter.SetProperty(RegionManager.RegionName, regionName);
window.Content = presenter;
if (data != null) window.DataContext = data;
return window.ShowDialog();
}
}
One technique I use is what I call the Navigation service. Note this is different from WPF's built in navigation framework. Your viewmodel could have an instance injected directly or you can use the EventAggregator pattern to fire a request to navigate that then gets handled by the navigation service. Having the navigation service injected directly means that it can be injected with other objects like ViewModelFactories. Regardless how you do it, at some point you're going to have to have an object that knows how to create the viewmodel properly resolved by your container.