Lifetime for the view / viewmodel when using data templates - wpf

I am defining a strategy where a main view will use data templates to switch between the views. Currently it can switch between 3 Views:
ApplicationView: it is actually the view that consists of lots of
different views, mostly layered out using tabs / docking. this is a
view that deals with application data.
LogInView: it is used for logging the user in.
DialogView: it is used to display dialog views. This view will also use data templates to select a proper view that is required.
The idea is that when a dialog view needs to be displayed, it is set as current view on the main view. After the selection is done, this information is passed to ApplicationView, or a view that is part of ApplicationView. While DialogView is shown, ApplicationView, must not be released from memory, since it ApplicationViewModel will still be manipulating with data (it needs to constantly work in the background).
I am thinking of achieving this using DataTemplates, and binding ContentControl's Content to CurrentView:
// in MainView
DataTemplate DataType="{x:Type vm:ApplicationViewModel}">
<vw:ApplicationView />
</DataTemplate>
.....
// in MainViewModel
public ViewModelBase CurrentView { get; set; }
Basically I am trying to avoid using modal windows for dialogs.
1) Is this strategy OK, or there are some problems that I am not aware with it?
2) When I switch to DialogView (I am actually switching viewmodels), what happens with the ApplicationView/ApplicationViewModel? Do I need to store ApplicationViewModel's reference somewhere, so it doesn't get garbage collected? I haven't tested this, but probably when I set CurrentView a new instance of ViewModel/View will be created.
3) Connected to second question, when using DataTemplates, what happens to View/ ViewModel that was previously used, and is now replaced with different view/viewmodel?

I don't see anything wrong with the way you're switching views, although typically you don't want to get rid of the application when you're displaying a dialog.
What I've done in the past is to put both the CurrentView, and the DialogView in a Grid so they are positioned on top of each other, then have the ApplicationViewModel contain an IDialogViewModel and IsDialogVisible properties, and when you want to display the dialog simply populate those two fields. (see below for an example)
You will have to store the ApplicationViewModel somewhere if you want to go back to it and avoid creating a new ApplicationViewModel
WPF disposes of UI objects that are no longer visible, so switching the CurrentView from Login to Application will get rid of the LoginView and create an ApplicationView
The ContentControl's Content is getting set to your ViewModel, so the ViewModel is actually being put in the applications VisualTree. Whenever WPF encounters an object in its VisualTree that it doesn't know how to draw, it will draw it using a TextBlock containing the .ToString() of the the object. By defining a DataTemplate, you are telling WPF how to draw the object instead of using its default .ToString() method. Once the object leaves the VisualTree, any visual objects that were created to render the object will get destroyed.
Although I would keep using what you currently have for switching Views, I would not use that method for the Login, Application, and Dialog views.
Typically the LoginView should only be displayed once when logging in, although it might get displayed again in a Dialog if you allow users to switch logins once logging in. Because of this, I typically show the LoginView in the startup code, then display the ApplicationView once login is successful.
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var login = new LoginDialog();
var loginVm = new LoginViewModel();
login.DataContext = loginVm;
login.ShowDialog();
if (!login.DialogResult.GetValueOrDefault())
{
Environment.Exit(0);
}
// Providing we have a successful login, startup application
var app = new ApplicationView();
var context = new ApplicationViewModel(loginVm.CurrentUser);
app.DataContext = context;
app.Show();
}
Like I said earlier, I wouldn't want to hide the Application when I display a Dialog, so I would make the Dialog part of the Application
Here's an example of the DataTemplate I would use for my ApplicationViewModel, using my own custom Popup from my blog for the Dialog
<Grid x:Name="ApplicationView">
<ContentControl Content="{Binding CurrentView}" />
<local:PopupPanel x:Name="DialogPopup"
Content="{Binding DialogContent}"
local:PopupPanel.IsPopupVisible="{Binding IsDialogVisible}"
local:PopupPanel.PopupParent="{Binding ElementName=ApplicationView}" />
</Grid>

Personally, I would find it easier to use ZOrdering within a standard grid, and put everything in the same view - using the ViewModel to manage visibility.
E.g
<Grid>
<Grid Visibility="{Binding IsView1Visible,
Converter={StaticResource BoolToVisibilityConverter}}">
<!-- view 1 contents -->
</Grid>
<Grid Visibility="{Binding IsView2Visible,
Converter={StaticResource BoolToVisibilityConverter}}">
<!-- view 2 contents -->
</Grid>
<Grid Visibility="{Binding IsView3Visible,
Converter={StaticResource BoolToVisibilityConverter}}">
<!-- view 3 contents -->
</Grid>
<Grid Visibility="{Binding IsDialogVisible,
Converter={StaticResource BoolToVisibilityConverter}}">
<!-- dialog contents contents -->
</Grid>
</Grid>

Related

How to have dynamically loaded instances of unbound named XAML elements behave independently from each other?

I have a WPF application where I dynamically load document view instances into a TabControl. The view has a ToolBar with some ToggleButtons which I use to control the visibility of certain elements in that view like so (only relevant elements shown):
<UserControl x:Class="MyProject.View.Views.DocumentView" ...>
...
<ToolBar>
<ToggleButton x:Name="togglePropInspector" ... />
...
</ToolBar>
...
<Border Visibility={Binding ElementName=togglePropInspector, Path=IsChecked, Converter={StaticResource BoolToVisibilityConverter}}">
...
</Border>
</UserControl>
I found this kinda neat as everything is handled inside the view and didn't require adding code to the view model (or code behind). However, the problem is that checking the toggle button on one tab now checks it in all instances of the view, not just the current tab. This basically applies to all elements whose state is not bound to the view model in any way. Is there a way around this without having to add code to the view model?
For completeness' sake here's the relevant part of how I'm loading the views:
<TabControl ItemsSource="{Binding Documents}">
<TabControl.Resources>
<DataTemplate DataType="{x:Type viewModels:DocumentViewModel}">
<local:DocumentView />
</DataTemplate>
</TabControl.Resources>
</TabControl>
TabControl has a single content host that is used for all TabItem instances. When the data models assigned to the TabItem.Content property are of the same data type, then the TabControl will reuse the same DataTemplate, which means same element instances, only updated with the changed data from data bindings.
To change the state of the reused controls, you must either access the control explicitly, or force the TabControl to reapply the ContentTemplate by temporarily changing the data type of the Content:
<TabControl SelectionChanged="TabControl_SelectionChanged" />
private void TabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var tabControl = sender as TabControl;
var tabItemContainer = tabControl.ItemContainerGenerator.ContainerFromItem(tabControl.SelectedItem) as TabItem;
object currentContent = tabItemContainer.Content;
tabItemContainer.Content = null;
// Defer and leave the context to allow the TabControl to handle the new data type (null).
// The content switch shouldn't be noticable in the GUI.
Dispatcher.InvokeAsync(() => tabItemContainer.Content = currentContent);
}
You can also use a dedicated data type for each tab. This way the TabControl is automatically forced to switch the DataTemplate.
The cleanest solution would be to bind the ToggleButton to the data model.

WPF multiple views [duplicate]

I am working on a an WPF MVVM application where I need to have a Main Window with just a logo and it has to show child views inside it. I don't have any controls in Main Window all the controls reside in child view for example Buttons like Next, Back, Cancel and some text blocks etc. Now If users select Next button on the child view I have to draw or load the next child view inside the Main Window. If Back button is clicked I have to go back to the previous child view. So basically I am changing the child views depending on which button is clicked. Also I am maintaining different view models for every child view. Now the problem is I am not able to figure how should I link the child views to there respective view models. This application is similar to some Installation applications where different dialogs are shown depending on the selection and the button clicked by the user.I am new to this wpf and don't want to use MVVM Light , Prism etc. Any detailed help will be greatly appreciated. Thanks in advance.
One of the easiest ways to associate any data type with XAML controls is to use a DataTemplate. Therefore, you can simply add something like this into your Application.Resources and as long as you do not set the x:Key properties on the DataTemplates, then they will be explicitly applied by the Framework whenever it comes across instances of your view models:
<DataTemplate DataType="{x:Type ViewModels:HomeViewModel}">
<Views:HomeView />
</DataTemplate>
...
<DataTemplate DataType="{x:Type ViewModels:MainViewModel}">
<Views:MainView />
</DataTemplate>
Then displaying the view is as simple as this:
<ContentControl Content="{Binding YourViewModelProperty"} />
In code behind, or your view model:
YourViewModelProperty = new MainViewModel();
It's often handy to create a base class for your view models and then the YourViewModelProperty can of that type and you will be able to interchange them using the same property and ContentControl.
UPDATE >>>
The general idea is that you have one MainViewModel class with one BaseViewModel property data bound to one ContentControl in MainWindow.xaml... the navigation controls should also be in MainWindow.xaml and not in the views themselves. In this way, the MainViewModel class is responsible for changing the property to the relevant view model instances when it receives navigation Commands from the MainWindow.xaml.

Multiple windows in WPF and the ability to tile them

I am writing a terminal in WPF, which communicates the host with an embedded device through RS232.
I want to be able to open multiple connections that will reside on different tabs, I believe that for that purpose WPF's tabContorl is sufficient, However the customer wants to be able to tile the different tabs on the screen, and as I understand the basic tabControl doesn't allow you to do that.
Any ideas how can this be done?
any help will be appreciated,
Thanks in advance.
Maybe it's overkill, but I would give a try to Avalon Dock from the WPF Toolkit, it's free. With that, you will be able to move terminals around, dock them wherever you wish and even have only opened at a time if you unpin others.
I have made a custom WPF control called ModalContentPresenter that allows you to display modal content which I think will be suitable. The control allows you to conditionally display additional content on top of your primary content.
Your application window will comprise a single ModalContentPresenter which will contain your TabControl in it's primary content and an ItemsControl in it's modal content.
<c:ModalContentPresenter IsModal="{Binding IsTiling}">
<DockPanel>
<Button Content="Show tiled view"
Command={Binding ShowTiledViewCommand}
DockPanel.Dock="Top"/>
<TabControl ItemsSource="{Binding Connections}"> />
</DockPanel>
<c:ModalContentPresenter.ModalContent>
<DockPanel>
<Button Content="Hide tiled view"
Command={Binding HideTiledViewCommand}
DockPanel.Dock="Top"/>
<Itemscontrol ItemsSource="{Binding Connections}" />
</DockPanel>
</c:ModalContentPresenter.ModalContent>
</c:ModalContentPresenter>
By default the modal content will not be displayed so all the user will see is the TabControl.
Both the TabControl and ItemsControl are bound to the same collection of data in your viewModel ensuring that the tiled view is in sync with the items in the tab control.
Your viewModel must have a Boolean property called IsTiling which should return true when you want the 'tiled' view to be shown and false when it is hidden.
When the tiled view is displayed users will be unable to interact with the primary content.
You can change the layout panel used by the ItemsControl to change how the collection of data is 'tiled'.
See this answer for an additional example of how to use the control.
The interface of your viewModel will look something like this:
public interface ViewModel {
public ObservableCollection<Connection> Connections { get; }
public boolean IsTiling { get; }
public ICommand ShowTiledViewCommand { get; }
public ICommand HideTiledViewCommand { get; }
}
Once you have this working you can add some additional enhancements to improve the look and feel of the user interface.
I would start by assigning a custom control template to the TabControl so that a Button is displayed in the 'header' area. This Button is bound to a command in your viewModel which is responsible for changing the IsTiling property to true.
This question provides a link to an answer which explores ways of achieving this.
A second enhancement is to remove the button from the modal content and call the HideTiledViewCommand command when the user selects an item in the items control. You can then add some additional logic which selects the correct tab when the tiled view is closed. I think this can be achieved by having an additional property in your viewModel representing the selected connection.

Unable to add two UserControls in the same view

I have a List View in which there are two Child Views. One is the Display View and another is Edit View. Here is how I have defined the List (Parent) view. Note that I want the two child UserControl's to occupy different space in the Parent.
<UserControl x:Class="RelayAnalysis_UI.Views.Relays.RelayListView"
....
<ContentControl x:Name="GroupDetail" Grid.Row="2" />
<TabControl x:Name="Items" Grid.Column="0" Style="{StaticResource TabControlStyle}" Margin="5,0,0,0"/>
</UserControl>
Then In my view model, I activate these items based on user responses in the following manner
**View Model **
[Export(typeof(RelayListViewModel))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class RelayListViewModel : Conductor<IScreen>.Collection.OneActive, IHandle<Group> {
....
public void Edit() { //Requested Edit
RelayEditViewModel viewModel = TryAndLocateViewModel(SelectedRelay.Group.Rack.Id, SelectedRelay.Group.Id);
ActivateItem(viewModel);
}
....
public void ViewGroupDetail(Relay relay) { //Requested View
GroupDetailViewModel viewModel = container.GetExportedValue<GroupDetailViewModel>();
ActivateItem(viewModel);
}
The above code works but the Detail View is loaded in the Tabs space (the space meant for the Edit View). Actually, the ActivateItem(viewModel) does pick up the correct type of view to display but it is loaded in the wrong place for the Display View, that is, the Display View is loaded in the Edit View's space on the screen. Surely I am missing some obvious stuff.
In summary, how do we get two UserControls defined in a Parent UserControl to activate in its own space?
Edit - 1:
Here are two Screen Shots which show where I need to load the Edit and Detail Views respectively.
As you can see in the second screenshot, the Detail View gets loaded in the Detail Area as well as the Edit Area(Tabs). I wan't the Detail View only to appear in the Detail Area. The Edit Area is only for the Edit View.
Here is the code that I have used to generate the screen shots.
The Main View that houses both views
<UserControl x:Class="RelayAnalysis_UI.Views.Relays.RelayListView"
<Grid>
....
<ContentControl x:Name="GroupDetail" HorizontalContentAlignment="Left"
cal:View.Context="GroupDetail" cal:View.Model="{Binding ActiveItem}"/>
<TabControl x:Name="Items" Grid.Column="0" Style="{StaticResource TabControlStyle}" Margin="5,0,0,0"
cal:View.Context="RelayEdit" cal:View.Model="{Binding ActiveItem}"/>
</Grid>
</UserControl>
Edit 2:
I think I am very close to get it working. As per your suggestions I modified the Main(Parent) container as below.
<UserControl x:Class="RelayAnalysis_UI.Views.Relays.RelayListView"
<ContentControl x:Name="GroupDetail" HorizontalContentAlignment="Left" />
<TabControl x:Name="Items" Grid.Column="0" Style="{StaticResource TabControlStyle}" Margin="5,0,0,0" />
The Edit Screen and Detail Screens now appear in their proper places. However, the Detail ViewModels OnActivate is not called upon so I get a blank Detail View with no variables populated. All loading of Details View field is done on the OnActivate() override. Here is how my GroupDetailViewModel is defined
[Export(typeof(GroupDetailViewModel))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class GroupDetailViewModel : Screen {
...
protected override void OnActivate() {
base.OnActivate();
..
}
So certainly, I am missing some attribute. Or will I have to call some method on the GroupDetailViewModel to load the details manually ?
Removed original answer because it was long and doesn't really help out much
Edit:
Ok so disregard the above - it looks like you are trying to load two different views over two different viewmodels, which as far as I know is not what Context is designed for. The Context property loads two different views over the same viewmodel e.g. in your XAML:
<ContentControl x:Name="GroupDetail" HorizontalContentAlignment="Left"
cal:View.Context="GroupDetail"
cal:View.Model="{Binding ActiveItem}"/>
<TabControl x:Name="Items" Grid.Column="0" Style="{StaticResource TabControlStyle}" Margin="5,0,0,0"
cal:View.Context="RelayEdit"
cal:View.Model="{Binding ActiveItem}"/>
Given a VM with a name of RelayEditViewModel activated via ActivateItem() CM will try to load the following views:
RelayEdit.GroupDetail for the content control
RelayEdit.RelayEdit for the tab control
See:
http://caliburnmicro.codeplex.com/wikipage?title=View%2fViewModel%20Naming%20Conventions&referringTitle=Documentation
...
If you try to load another ViewModel, the same conventions will apply to find the view
GroupDetailViewModel results in
GroupDetail.GroupDetail for the content control
GroupDetail.RelayEdit for the tab control
It sounds like this isn't what you want (and I'm not sure why anything is loading at all - what namespace are your views in? Have you customised the view locator?)
I'm still trying to get my head round the lifecycle support you require but it sounds like you want the edit view to be lifecycle managed (since you want the load/save/guard type support) but the detail view is to be read-only and doesn't care if it's closed without being guarded
In that case you probably want to add a property to your ViewModel which will hold a reference to the details viewmodel but don't activate it ... just set the property without calling ActivateItem(vm)
example:
[Export(typeof(RelayListViewModel))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class RelayListViewModel : Conductor<IScreen>.Collection.OneActive, IHandle<Group> {
....
// Backing field + prop to hold the details view - the content control will bind to this
private IScreen _details;
public IViewAware Details { get { } set { } } // Implement standard NotifyOfPropertyChange here for this property
public void Edit() { //Requested Edit
RelayEditViewModel viewModel = TryAndLocateViewModel(SelectedRelay.Group.Rack.Id, SelectedRelay.Group.Id);
ActivateItem(viewModel);
}
....
public void ViewGroupDetail(Relay relay) { //Requested View
GroupDetailViewModel viewModel = container.GetExportedValue<GroupDetailViewModel>();
// Instead of activating, just assign the VM to the property and make sure Details calls NotifyOfPropertyChange to let CM know to start the binding logic
Details = viewModel;
}
Then in your XAML
<!-- Just bind the details view to the Details property -->
<ContentControl x:Name="Details" HorizontalContentAlignment="Left" />
<!-- Leave this as-is, as it's working ok -->
<TabControl x:Name="Items" Grid.Column="0" Style="{StaticResource TabControlStyle}" Margin="5,0,0,0" />
(I've assumed that you are using the TabControls default conventions above, but tweak if neccessary)
You can use the same VM for both the details and the edit view as long as you set the Context property accordingly.
Let me know if that helps
Edit:
Just to answer the question about MVVM and coupling etc...
All you are doing is composing a more complex viewmodel from several simpler viewmodels (and therefore a more complex view from several simpler views). As long as your reference to the details VM is not a concrete type, there is very loose coupling between the VMs. You could assign ANY viewmodel type that implements that interface into the Detail property on the main VM and CM will try to locate the view for it and build the interface. This is perfectly fine (you can use your IoC to get the type for the details window if needed)
If your details view needs lifecycle you should inherit from Screen, but I doubt that your details view needs activation (since it's just a details view and is ready only) so just implementing IViewAware and inheriting from PropertyChangedBase will do. The edit view, however, needs to have lifecycle and therefore should inherit from Screen.
Conductor already contains an ActiveItem property, and provides management of lifecycle for child items activated via ActivateItem(), all you are doing is creating an extra 'bolt-on' property for your conductor which references the additional vm (i.e. you need ActiveItem and Details)

Delay load user controls in parent control

I have the scenario where I have some User controls Let say they are :
CreateStudents
CreateTeachers
Each of the User control have their own View model. The datacontext is set in initialization.
I have a main UI where these above User control can be loaded. So setting the datacontext works fine.
Problem statement
I have another User control "CreateClass" which is a collection of tabs. From here I can go to the above two user cotrols (Hidden tabs) .
The datacontext is set by two properties in CreateClassViewModel "CreateStudentManager" and "CreateTeacherManager".
This works fine but the issue is when i default open CreateClass UI, the other User controls also load (I guess because they have default constructor).
Because when I open CreateClass I donot want other controls to be loaded. These should be only loaded when they are called from the Create Class UI explictly.
How to achieve this ?
below is sample for one "CreateStudent"
<TabItem Header="Students" Visibility="{Binding IsStudentVisible, Converter={StaticResource BooleanToVisibilityConverter}}" >
<Grid>
<local:UCCreateStudent DataContext="{Binding CreateStudentManager}"/>
</Grid>
</TabItem>
Girija
Easiest would be to do it in code, ie. add the local:UCCreateStudent item to the grid on the desired event trigger. Give the grid a name (e.g. x:Name="MyGrid"), then
void OnTrigger(...)
{
UCCreateStudent NewStudent = new UCCreateStudent();
// extra code for setting the datacontext and any other layout properties
MyGrid.Children.Add(NewStudent);
}
There is a good article on codeproject:
http://www.codeproject.com/Articles/217022/Delaying-Element-Initialization-for-Collapsed-Cont

Resources