This is a sample .xaml code that I have, I'm using DrawerHost Control from MaterialDesignInXamlToolkit
<UserControl
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DataContext="{d:DesignInstance viewModels:UserControlViewModel}"
mc:Ignorable="d"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes">
<materialDesign:DrawerHost IsRightDrawerOpen="{Binding IsDrawerOpen}" OpenMode="Default">
<materialDesign:DrawerHost.RightDrawerContent>
<views:RightDrawerView />
</materialDesign:DrawerHost.RightDrawerContent>
<!-- Main Content -->
</materialDesign:DrawerHost>
</UserControl>
RightDrawerViewModel will be set to be the DataContext of RightDrawerView via Prism's ViewModelLocationProvider.
My Question: When setting IsDrawerOpen to true, how can UserControlViewModel pass parameters to RightDrawerViewModel? as RightDrawerViewModel is not called via Prism's methods (regionManager?.RequestNavigate, dialogService?.ShowDialog).
If you want to pass parameters, you can either RequestNavigate instead of setting IsDrawerOpen (which needs a region and an custom RegionAdapter for the DrawerHost) or you create a service known to both the view model that wants to open the drawer and the drawer's view model and put all data there before setting IsDrawerOpen.
A third option is to create a new DrawerViewModel when you want to open the drawer and assign it to a property on the parent view model and bind it to the drawer's content's data context. Also, remove IsDrawerOpen and replace it either with a style or a converter that observe the view model-property on the parent.
I'd go for the first option only if I were forced to go view first, otherwise always prefer the third one. The second's ugly and presented for completeness only.
My Solution..
First, Define NavigationParameters property in RightDrawerViewModel
public class RightDrawerViewModel {
public NavigationParameters NavigationParameters { set; get; }
}
In my case, I have predefined BaseViewModel that implements INavigationAware, so I've added NavigationParameters property to it (this way I can hold the parameters that are passed via RequestNavigate even for other ViewModels that inherit from BaseViewModel and use this property for the rest of the OnNavigatedTo code)
public class RightDrawerViewModel : BaseViewModel {
// ..
}
public abstract class BaseViewModel : INavigationAware {
public NavigationParameters NavigationParameters { set; get; } // parameters holder
public void OnNavigatedTo(NavigationContext navigationContext)
{
if (navigationContext?.Parameters != null)
NavigationParameters = navigationContext.Parameters;
// ..
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
NavigationParameters = null;
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
// ..
}
Then, Pass the parameters before setting IsDrawerOpen to true..
// 1. Let UserControl class implement an interface that has `DataContext GetRightDrawerDataContext();` method,
// 2. Inject services instnace in ctor of UserControlViewModel via Prism's Ioc Container
if (services.GetRightDrawerDataContext() is INavigationAware nv)
nv.NavigationParameters = parameters; // parameters to pass
IsDrawerOpen = true;
Finally, RightDrawerViewModel can get the parameters from NavigationParameters property.
var b = NavigationParameters?.TryGetValue("Parameter1Key", out object object1Instance);
Related
I have a window that displays templates in a tree, these can be selected which updates a ListView with available fields in the template. All related operations until here are managed by the TemplateViewModel declared at windows level as:
<Window.DataContext>
<vm:TemplateViewModel/>
</Window.DataContext>
extract of the class:
public class TemplateViewModel : ViewModelBase,INotifyPropertyChanged
{
public FieldTypeViewModel FieldTypeView { get; }
public TemplateViewModel()
{
// Create additional view
FieldTypeView = new FieldTypeViewModel(this);
...
}
Each template field has an identifier and type which are still managed by this view (all working up to here).
Now depending on the type of the field a different page is to be displayed in a reserved window part (Frame). Also the type view model is a separate view model class FieldTypeView .
The FieldType object is created in the constructor of the TemplateViewModel and saved in the FieldTypeView property as it needs to be linked to this model for updating as field gets selected.
Both views used to implement the INotifyPropertyChanged interface but since the FieldTypeView is created by the view and not by the window defintion the notification event is not set, so I currently call the parent (TemplateViewModel) event for notification.
So I have a frame defined as:
<Frame DataContext="{Binding FieldTypeView}" Grid.Row="1" Content="{Binding CurrentFieldTypeSetupPage}"/>
public class FieldTypeViewModel : ViewModelBase
{
private TemplateViewModel _templateViewModel;
private TTemplateFieldType? _FieldType;
public TTemplateFieldType? FieldType
{
get { return _FieldType; }
set { _FieldType = value;
UpdateFieldType();
NotifyPropertyChanged("FieldType"); }
}
private Page? _CurrentFieldTypeSetupPage;
public Page? CurrentFieldTypeSetupPage
{
get { return _CurrentFieldTypeSetupPage; }
set { _CurrentFieldTypeSetupPage = value; NotifyPropertyChanged("CurrentFieldTypeSetupPage"); }
}
// Define property per type for easy data context access
public TTFTText? tfText { get; set; }
public TTFTDate? tfDate { get; set; }
//------------------------------------------------------------------------------
private void UpdateFieldType()
{
// Set the appropriate field type, and "null" the others
tfText = _FieldType as TTFTText;
tfDate = _FieldType as TTFTDate;
if (_FieldType != null)
{
CurrentFieldTypeSetupPage = _FieldType.GetSetupPage();
}
else
{
CurrentFieldTypeSetupPage = null;
}
}
//------------------------------------------------------------------------------
public void NotifyPropertyChanged(string prop)
{
_templateViewModel.NotifyPropertyChanged(prop);
}
//------------------------------------------------------------------------------
public FieldTypeViewModel(TemplateViewModel templateVM)
{
_templateViewModel = templateVM;
}
}
Every time the field selection changes the TemplateViewModel does set the FieldTypeView which gets the correct window for the current type and sets its CurrentFieldTypeSetupPage, which finally notifies the change via NotifyPropertyChanged("CurrentFieldTypeSetupPage"); which actually calls the TemplateViewModel's NotifyPropertyChanged method calling the event handler to notify the change.
Note that notification in the TemplateViewModel works for all its other fields, but the type page is never shown.
So the question is what I am doing wrong or what is the correct way to implement dynamic page changing in MVVM. My guess is that INotifyPropertyChange is not the correct way to go ?
I have a Window with a Grid, which has a "MainWindowViewModel" set as its DataContext
<Grid x:Name="MainGrid">
<Grid.DataContext>
<view:MainWindowViewModel/>
</Grid.DataContext>
<!-- ... -->
</Grid>
This MainGrid has two SubGrids (not named) and one of them contains a Frame which displays Pages.
The Pages displayed have other ViewModels set as their DataContext.
<Page.DataContext>
<view:AddOrderViewModel/>
</Page.DataContext>
In the MainWindowViewModel I have a Property "User". I want to access this Property from the ViewModel of the Page.
Is that even possible (without using "code behind"). I dont really know where to start since I dont know how to get the FrameworkElement using the ViewModel from within the ViewModel (I guess from there its only handling the visual tree?)
Any help, or push in the right direction would be greatly appreciated. Also if you have a better idea of how to pass the property from one ViewModel to the other, feel free to share :)
Thanks
I would suggest to try MVVM Light's Messenger.
It is thoroughly enough explained here
You create a class where you place the object property you want to send between ViewModels
public class MessageClassName
{
public object MyProperty { get; set;}
}
Assuming you want to send the property from ViewModel1 to ViewModel2, you create a method in ViewModel1
private void SendProperty(object myProperty)
{
Messenger.Default.Send<MessageClassName>(new MessageClassName() { MyProperty = myProperty });
}
Then you are calling it from your code when you want it to be sent.
SendProperty(_myProperty);
In the constructor of ViewModel2 you register to that message
public ViewModel2()
{
Messenger.Default.Register<MessageClassName>(this, (message) =>
{
ReceiveProperty(message.MyProperty);
)};
}
Then also in ViewModel2 you define the method ReceiveProperty
private void ReceiveProperty(object myProperty)
{
...Do whatever with myProperty here...
}
Note that you need to add
using GalaSoft.MvvmLight.Messaging;
in both ViewModel1 and ViewModel2 classes
I have a main view in which I have a tab control. The content of each tab is a treeview which is present in different views.
This is my main view in which I use 2 other views
In my FirstListView, I have a tree view, a textbox and a button.
<TabControl x:Name ="MainTab" SelectionChanged="OnTabSelectionChanged">
<TabItem Header="First" >
<view:FirstListView x:Name="FirstView"/>
</TabItem>
<TabItem Header="Second" >
<view:SecondListView x:Name ="SecondView"/>
</TabItem>
</TabControl>
Textbox and the button are added to perform a search in the tree.
The view model associated with the FirstListView has a command that is initialized in its contructor.
_searchCommand = new SearchFamilyTreeCommand(this);
SearchFamiltyTreeCommand is a class that is derived from ICommand and the execute method calls a function to perform the search. This is present in the FirstViewModel.
#region SearchCommand
public ICommand SearchCommand
{
get { return _searchCommand; }
}
private class SearchFamilyTreeCommand : ICommand
{
readonly FunctionListViewModel _functionTree;
public SearchFamilyTreeCommand(FunctionListViewModel functionTree)
{
_functionTree = functionTree;
}
public bool CanExecute(object parameter)
{
return true;
}
event EventHandler ICommand.CanExecuteChanged
{
add { }
remove { }
}
public void Execute(object parameter)
{
_functionTree.PerformSearch();
}
}
#endregion
The search method is not type independent. It depends on the type present in its particular model. And the data required to perform the search is present in this view model.
This is working. Now I have to extend this functionality to other views ( SecondListView, ThirdListView and so on) which have their own treeviews(the type of the content is different from the FirstTreeView). How can I do it? Where shall I place the code and the command?
Don't place business Logic into ViewModels. ViewModels should be only for presentation logic.
Create a FamilyTreeSearchService and abstract it's functionality to this service, then inject the service into your ViewModels (i.e. Constructor, Dependency Injection, or ServiceLocator). Call the service from your ViewModels.
1) Proper way:
Inherit your ViewModel classes directly from a common abstract base class. Refer this Stackoverflow Answer
2) Simple Way:
Have a separate class naming like 'CommonViewModel' and have common code in it. Inherit your additional ViewModel classes from the CommonViewModel;
Like below,
public class CommonViewModel
{
....
}
public class FirstViewModel:CommonViewModel
{
....
}
I'm new on Caliburn Micro and want some advice on which path to take to devolop my app interface and navigation between views.
My idea is to have a MainWindow which will contain a menu of buttons, each one related with a specific view. Each view will be stored in a separated WPF UserControl. The mainWindow will also contain a TabControl bound to an ObservableCollection of tabs on viewmodel. Everytime a button on menu is clicked, I want to add a new tab with a ContentPresenter inside that will dynamically load a view and its corresponding viewmodel.
So my questions:
1) Should I use a Screen Collection here?
2) Should the UserControl implement Screen interface?
3) How do I tell MainWindow ViewModel which view to load on the new added tab maintaining viewmodels decoupled?
Thanks to everyone in advance.
UPDATE
After a lot of reading and some help of the community I managed to resolve this. This is the resultant AppViewModel:
class AppViewModel : Conductor<IScreen>.Collection.OneActive
{
public void OpenTab(Type TipoVista)
{
bool bFound = false;
Screen myScreen = (Screen)Activator.CreateInstance(TipoVista as Type);
myScreen.DisplayName = myScreen.ToString();
foreach(Screen miItem in Items)
{
if (miItem.ToString() == myScreen.ToString())
{
bFound = true;
ActivateItem(miItem);
}
}
if (!bFound) ActivateItem(myScreen);
}
public ObservableCollection<MenuItem> myMenu { get; set; }
public ObservableCollection<LinksItem> myDirectLinks { get; set; }
public ICommand OpenTabCommand
{
get
{
return new RelayCommand(param => this.OpenTab((Type) param), null);
}
}
public AppViewModel()
{
OpenTab(typeof(ClientsViewModel));
MenuModel menu = new MenuModel();
myMenu = menu.getMenu();
myDirectLinks = menu.getLinks();
}
public void CloseTab(Screen param)
{
DeactivateItem(param, true);
}
}
I have to keep the ICommand from OpenTabCommand because the name convention of Caliburn.micro doesn't seems to work inside DataTemplate. Hope it could help someone else. Thanks to all
I've done something very similar using Caliburn.Micro, and based it on the SimpleMDI example included with the examples, with a few tweaks to fit my needs.
Much like in the example, I had a main ShellViewModel:
public class ShellViewModel : Conductor<IScreen>.Collection.OneActive
{
}
with a corresponding ShellView containing a TabControl - <TabControl x:Name="Items">, binding it to the Items property of the the Conductor.
In this particular case, I also had a ContextMenu on my ShellView, bound (using the Caliburn.Micro conventions), to a series of commands which instantiated and Activated various other ViewModels (usually with a corresponding UserControl, using the ActivateItem method on the Conductor.
public class YourViewModel: Conductor<IScreen>.Collection.OneActive
{
// ...
public void OpenItemBrowser()
{
// Create your new ViewModel instance here, or obtain existing instance.
// ActivateItem(instance)
}
}
In that case, I didn't require the ViewModels to be created with any particular dependency, or from any other locations in the program.
At other times, when I've needed to trigger ViewModel from elsewhere in the application, I've used the Caliburn.Micro EventAggregator to publish custom events (e.g. OpenNewBrowser), which can be handled by classes implementing the corresponding interface (e.g. IHandle<OpenNewBrowser>), so your main ViewModel could have a simple Handle method responsible for opening the required View:
public class YourViewModel: Conductor<IScreen>.Collection.OneActive, IHandle<OpenNewBrowser>
{
// ...
public void Handle(OpenNewBrowser myEvent)
{
// Create your new ViewModel instance here, or obtain existing instance.
// ActivateItem(instance)
}
}
This section of the documentation will probably be useful, especially the Simple MDI section.
Additional code I mentioned in the comments:
I sometimes use a generic method along these lines ensure that if I have an existing instance of a screen of a particular type, switch to it, or create a new instance if not.
public void ActivateOrOpen<T>() where T : Screen
{
var currentItem = this.Items.FirstOrDefault(x => x.GetType() == typeof(T));
if (currentItem != null)
{
ActivateItem(currentItem);
}
else
{
ActivateItem(Activator.CreateInstance<T>());
}
}
Used like:
public void OpenBrowser()
{
this.ActivateOrOpen<BrowserViewModel>();
}
In my Viewmodel I have the properties LoggedInAs of type string and EditMode of type bool. I also have a List property called ReaderList which I bind to an ItemsControl for display purposes like this:
<ItemsControl Name="ReaderList" ItemTemplateSelector="{StaticResource drts}"/>
I am using Caliburn.Micro, so the Binding is done automatically by the naming. I want to use a DataTemplateSelector because if the application is in EditMode and the Person is the one that is logged in I want a fundamentally different display. So here is my declaration of the resources,
<UserControl.Resources>
<DataTemplate x:Key="OtherPersonTemplate"> ... </DataTemplate>
<DataTemplate x:Key="CurrentUserIsPersonTemplate"> ... </DataTemplate>
<local:DisplayReaderTemplateSelector x:Key="drts"
IsLoggedInAs="{Binding LoggedInAs}"
IsEditMode="{Binding EditMode}"
CurrentUserTemplate="{StaticResource CurrentUserIsPersonTemplate}"
OtherUserTemplate="{StaticResource OtherPersonTemplate}"/>
</UserControl.Resources>
and here the code for the class:
public class DisplayReaderTemplateSelector: DataTemplateSelector {
public DataTemplate CurrentUserTemplate { get; set; }
public DataTemplate OtherUserTemplate { get; set; }
public string IsLoggedInAs {get; set;}
public bool IsEditMode { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container){
var _r = item as Person;
if (IsEditMode && _r.Name == IsLoggedInAs) return CurrentUserTemplate;
else return OtherUserTemplate;
}
}
For some reason the application crashes while instantiating the Viewmodel (resp. the View). Where is the error, and/or how could I solve this problem alternatively?
EDIT: The Crash was due to the binding expressions in the construction of the DisplayReaderTemplateSelector - because IsLoggedIn and EditMode are not DependencyProperties.
So the question now is: how can I have a DataTemplateSelector that depends on the status of the ViewModel if I cannot bind to values?
Whilst you could use a DataTemplateSelector or something of that ilk, it probably won't surprise you to find that in Caliburn.Micro has this functionality built-in in the form of View.Context and the ViewLocator
On your VM you can create a property which provides a context string which CM will use to resolve the View - since it uses naming conventions, you just need to provide the correct namespace/name for the sub-view along with a context string for it to locate an alternative view
In your VM you can create a context property that uses the user details to determine its value:
i.e.
public class SomeViewModel
{
public string Context
{
get
{
if (IsEditMode && _r.Name == IsLoggedInAs) return "Current";
else return "Other";
}
}
// ... snip other code
}
The only problem I see (one that probably has a workaround) is that you want to determine the view from inside a ViewModel - usually you determine the context higher up and pass that to a ContentControl and CM uses it when locating the view for that VM
e.g.
your main VM:
public class MainViewModel
{
public SomeSubViewModel { get; set; } // Obviously would be property changed notification and instantiation etc, I've just left it out for the example
}
and associated view
<UserControl>
<!-- Show the default view for this view model -->
<ContentControl x:Name="SomeSubViewModel" />
<!-- Show an alternative view for this view model -->
<ContentControl x:Name="SomeSubViewModel" cal:View.Context="Alternative" />
</UserControl>
then your VM naming structure would be:
- ViewModels
|
----- SomeSubViewModel.cs
|
- SomeSubView.xaml
|
- SomeSubView
|
----- Alternative.xaml
and CM would know to look in the SomeSubView namespace for a control called Alternative based on the original VM name and the Context property (SomeSubViewModel minus Model plus dot plus Context which is SomeSubView.Alternative)
So I'd have to have a play around as this is the standard way of doing it. If you were to do it this way you'd have to either create a sub viewmodel and add a ContentControl to your view and bind the View.Context property to the Context property on the VM, or add the Context property higher up (to the parent VM).
I'll look at some alternatives - if there is no way to get the current ViewModel to decide its view based on a property using standard CM, you could customise the ViewLocator and maybe use an interface (IProvideContext or somesuch) which provides the ViewLocator with a context immediately -(I don't think you can't hook directly into the view resolution process from a VM)
I'll come back with another answer or an alternative shortly!
EDIT:
Ok this seems to be the most straightforward way to do it. I just created an interface which provides Context directly from a VM
public interface IProvideContext
{
string Context { get; }
}
Then I customised the ViewLocator implementation (you can do this in Bootstrapper.Configure()) to use this if no context was already specified:
ViewLocator.LocateForModel = (model, displayLocation, context) =>
{
var viewAware = model as IViewAware;
// Added these 3 lines - the rest is from CM source
// Try cast the model to IProvideContext
var provideContext = model as IProvideContext;
// Check if the cast succeeded, and if the context wasn't already set (by attached prop), if we're ok, set the context to the models context property
if (provideContext != null && context == null)
context = provideContext.Context;
if (viewAware != null)
{
var view = viewAware.GetView(context) as UIElement;
if (view != null)
{
#if !SILVERLIGHT && !WinRT
var windowCheck = view as Window;
if (windowCheck == null || (!windowCheck.IsLoaded && !(new WindowInteropHelper(windowCheck).Handle == IntPtr.Zero)))
{
LogManager.GetLog(typeof(ViewLocator)).Info("Using cached view for {0}.", model);
return view;
}
#else
LogManager.GetLog(typeof(ViewLocator)).Info("Using cached view for {0}.", model);
return view;
#endif
}
}
return ViewLocator.LocateForModelType(model.GetType(), displayLocation, context);
};
This should work for you and allows you to set the context directly on the target ViewModel - obviously this will probably only work for a View-First approach
So all you need to do is structure your views as I showed above (the correct namespaces etc) then set the Context property on your VM based on the value of IsLoggedInAs and EditMode