How to make this DataTemplateSelector work? - c#

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

Related

WPF - Prism - how to pass parameters to Drawer's ViewModel

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

How can I test a ViewModel that needs to coordinate the creation and hosting of UserControls?

I'm working on a demo MVVM project where I have a WPF MainWindow with a ViewModel that needs to coordinate the creation and hosting of different UserControls. If the ViewModel is not supposed to have any part of WPF elements I'm not sure how to go about doing this. I know this is a rather broad design question but I'm new to WPF/TDD and I'm having difficulty seeing a clear path as to how to create and bind a UserControl to a ViewModel without having some of the create and bind code IN the ViewModel.
From what I've read exposing a UserControl property in the MainViewModel that binds to a ContentControl is not the way to go. How can I abstract away the creation and binding of UserControls in my MainView model so I can test it?
Works but not testable:
<ContentControl Grid.Row="2" Content="{Binding UserControl}" />
public class MainWindowViewModel
{
public void ShowHome()
{
SomeUserControl uc = new SomeUserControl();
uc.DataContext = new SomeUserControlViewModel();
UserControl = uc;
}
public void ShowKeypad()
{
SomeOtherUserControl uc = new SomeOtherUserControl();
uc.DataContext = new SomeOtherUserControlViewModel();
UserControl = uc;
}
public UserControl UserControl {get; private set;}
}
simply use a DataTemplate. Let Wpf choose the View for you.
<ContentControl Grid.Row="2" Content="{Binding UserControl}" />
public class MainWindowViewModel
{
public void ShowHome()
{
MyViewmodelChoosedInMain = new SomeViewModel();
}
public void ShowKeypad()
{
MyViewmodelChoosedInMain = new SomeOtherViewModel();
}
//better use an Interface instead of object type ;)
//you also need to implement and call INotifyPropertyChanged of course
public object MyViewmodelChoosedInMain {get; private set;}
}
//in your ResourceDictionary create the DataTemplates
<DataTemplate DataType="{x:Type SomeViewModel}">
<MySomeViewmodelView />
</DataTemplate>
<DataTemplate DataType="{x:Type SomeOtherViewModel}">
<MySomeOtherViewmodelView />
</DataTemplate>
There are a couple of things you can do.
I have created many projects where the view at startup creates controls where the visibility is set to Hidden. Then the VM creates/has a State property which defines the different states of the app. As that property is changed (via INotifyPropertyChanged) controls on the screen appear or hide themselves.
Working with #1 or without, one can create commands that the view can process but which are initiated by the VM or elsewhere. Hence keeping separation of concerns.
#1
Define Enum
public enum OperationState
{
Select = 1,
Routine,
Alignment,
SerialNumber,
}
On VM define state
private OperationState _State;
public OperationState State
{
get => _State;
set { _State = value; OnPropertyChanged(nameof(State)); }
}
Set state as needed such as State = Select.
Have control(s) viewability based on state
<Control:AlignmentProcessing
ProjectContainer="{Binding CurrentContainer, Mode=TwoWay}"
Visibility="{Binding State, Converter={StaticResource VisibilityStateConverter},
ConverterParameter=Alignment}"/>
The above control will only be visible during the Alignment state.
Converter Code
/// <summary>
/// Take the current state, passed in as value which is bound, and check it against
/// the parameter passed in. If the names match, the control should be visible,
/// if not equal, then the control (window really) should be collapsed.
/// </summary>
public class OperationStateToVisibility : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (value != null) &&
(parameter != null) &&
value.ToString().Equals(parameter.ToString(), StringComparison.OrdinalIgnoreCase)
? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
#2 Otherwise implement a commanding operation on the VM such as:
public ICommand ShowControl1 { get; set; }
Then on the View subscribe to the command (assuming VM holds the current VM):
VM.ShowControl1 = new Commanding((o) =>
{
SomeUserControl uc = new SomeUserControl();
uc.DataContext = new SomeUserControlViewModel();
UserControl = uc;
}
Then when the command is executed (from the VM) the view does its work.
ShowControl1.Execute(null);
I provide a commanding example on my blog Xaml: MVVM Example for Easier Binding
The view-model should not have any controls in it with the MVVM pattern. A view-model is simply a state of the data to be displayed.
For example:
Record(s) to display
Title
Image
Can Delete
Can Edit
In WPF, the controls can bind to these properties for it's display.
View-Models are view agnostic (they don't care which view is consuming it). And so no actual reference to UI controls would be in them.
To test the actual UI, you can write a "coded UI test". In the web app works, there is the Selenium framework that allowed you to write unit tests for interacting with UI components in a browser.
I'm pretty sure there is a similar framework it there for WPF UI testing.
Edit: There is a framework called Appium that allows you to write the integration tests between your UI and the underlying MVVM setup you have.
http://appium.io/docs/en/drivers/windows/

Advice on Views navigation using Caliburn.Micro MVVM WPF

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

WPF MVVM one view multiple object types

Currently I'm learning WPF with MVVM and have maybe a crazy idea...
I have several simple classes:
public class Car : IProduct
{
public int Id {get;set;}
public string Brand {get;set;}
// some custom properies
}
public class Seat : IProduct
{
public int Id {get;set;}
public string Brand {get;set;}
// some custom properties
}
Idea was that I have one editor view for diferent models.
public class ProductViewModel<T> : ViewModelBase, IProductViewModel<T> where T : IProduct
{
private T m_editorModel;
public T EditorModel
{
get { return m_editorModel; }
set
{
m_editorModel = value;
RaisePropertyChanged(() => EditorModel);
}
}
public Type ModelType
{
get { return typeof(T); }
}
}
Which can be afterwords set to view DataContext
viewModel = ViewModelFactory.CreateViewModel<IProductViewModel<Car>>();
view = ViewFactory.CreateView<ProductView>();
view.DataContext = viewModel;
// etc...
The problem is that I don't know is it possible or how to create in run time
ObservableCollection of same object EditorModel.
Is it maybe easier path to create for each class it's own view and viewmodel or something totally different?
In MVVM in general [I'm not speaking for everyone here], you don't want to be instantiating views from code. Instead we work with and manipulate data. To change views, we change view models and often set the connections between the two in simple DataTemplates:
<DataTemplate DataType="{x:Type ViewModels:MainViewModel}">
<Views:MainView />
</DataTemplate>
...
<DataTemplate DataType="{x:Type ViewModels:UsersViewModel}">
<Views:UsersView />
</DataTemplate>
This way, we don't need to explicitly set any DataContexts. We can simply have a BaseViewModel property that each view model extends:
public BaseViewModel ViewModel
{
get { return viewModel; }
set { if (viewModel != value) { viewModel = value; NotifyPropertyChanged("ViewModel"); } }
}
We can change view models and therefore views like this:
ViewModel = new UsersView();
Then we can display the relating view in a ContentControl like this:
<ContentControl Content="{Binding ViewModel}" />
Finally, in my opinion, you really should create a view model for each view... the view model's sole job is to provide the data and functionality for each view. So unless you have multiple identical views, you'd need different view models. It is however possible to have one view model that all of the views bind to, but I'd advise against that for large applications.

How can I open a page and assign its datacontext

my question is relatively easy, I guess.
I have a page to display my data. On a click on a button I want to open a new page with the datacontext of an element 2 layers above the datacontext of the current element.
Explanation:
My ViewModel is a class (ViewModelContainer) that contains more ViewModels. One is a summary of values and one is the detailed view of that.
public class SummaryViewModel
{
public int somevalue; // is a property
public ObservableCollection<SummarizedItems> items; // is a property
}
public class DetailsViewModel
{
public int someOthervalue; // is a property
public int stuffA; // is a property
public int stuffB; // is a property
}
public class ViewModelContainer : ViewModelBase
{
private SummaryViewModel _sum;
public SummaryViewModel sum { } // is a property
private DetailsViewModel _det;
public DetailsViewModel det { } // is a property
}
The View where I can press a button is bound to the value of the ObservableCollection of SummaryViewModel.
Everything is fine till now. When I press the button, a new page, showing the details should be opened. I use an ICommand to handle the click, and pass it the details view as a CommandParameter.
<Button Name="OpenDetailsButton" Command="{Binding Path=ACommand}" CommandParameter="{DynamicResource Details}"
I define a page as a resource in the same file, where the datacontext is still ViewModelContainer.
<pages:DetailsViewPage DataContext="{Binding Path=det }" x:Key="Details"/>
The page opens, but the datacontext is not available. I get the following error:
System.Windows.Data Error: 3 : Cannot find element that provides DataContext.
Has anyone an idea how I could open the details view and providing the datacontext? I cannot move the DetailsViewModel to another class, because it is only possible to update it there.
Thanks
I have solved the problem by creating a helper to go up the visual tree and use the datacontext of the element i needed. Thanks for everybody who tried to help :)
the method looks like this:
public static UIELEMENT FindUiElementUpVisualTree(DependencyObject initial)
{
DependencyObject current = initial;
while (current != null && current.GetType() != typeof(UIELEMENT))
{
current = VisualTreeHelper.GetParent(current);
}
return current as UIELEMENT;
}
where UIELEMENT is the object you are looking for e.g. a window or a button or something.
usually views and viewmodels have a one to one relationship. In this case it seems like there is a many to one relationship. How about a DetailsPageViewModel?

Categories

Resources