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)
Related
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.
I am new to WPF and the MVVM pattern. I am trying to build a 'step by step' or 'wizard' like application.
So the user should first login then select some data and finally the selected data should be stored somewhere. (This flow never changes!)
However I decided to use DataTemplates and different ViewModels for each Template and a MainViewModel for the Window which populates the Templates. (Think this should be ok regarding to different Posts here)
But now my problems are starting. I know how I can change the current view from the MainViewModel BUT I want to change the current View from the inner ViewModel. And I want to be able to collect data from one inner ViewModel to another and I have no clue how this can work.
MainViewModel:
public class MainWindowViewModel : ViewModelBase
{
public ViewModelBase CurrentView {get; set;}
public MainWindowViewModel()
{
CurrentView = new ViewModelA;
}
}
XAML:
<Window x:Class="PUSEImporter.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:VM="clr-namespace:PUSEImporter.ViewModel"
xmlns:V="clr-namespace:PUSEImporter.View">
<Window.DataContext>
<VM:MainWindowViewModel />
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType="{x:Type VM:ViewModelA}">
<V:Detail />
</DataTemplate>
<DataTemplate DataType="{x:Type VM:ViewModelB}">
<V:Overview />
</DataTemplate>
</Window.Resources>
<ContentPresenter Content="{Binding CurrentView}"/>
</Window>
So think about a button in ViewModelA (or the View from ViewModelA) and now I want to switch to ViewModelB when someone clicks on the button. And not enough the data which is collected by ViewModelA should also be available in ViewModelB.
Is this possible and the preferred way of using this techniques or do I misunderstand some concepts?
(And if this is true how should i handle things like that?)
Thanks in advance!
There are many ways to achieve what you want. In MVVM, there's one view model to each view... therefore, if your main view has a child view, then your main view model should have a property of the type of another view model. In this instance, you can add a delegate to the child view model and register a handler for it in the main view model.
This will enable you to effectively pass a signal to the main view model from the child view model, which you can react to in the parent view model in any way you want to. Rather than explain the whole story again here, please see my answers from the Passing parameters between viewmodels and How to call functions in a main view model from other view models? posts here on Stack Overflow for more information on how to achieve this.
I'm trying to get to grips with the concept of a ViewModelLocator (in MVVM Light, although the question applies to the concept of a ViewModelLocator in general, regardles of which MVVM framework is used), and I have a hard time figuring out how to use it.
As I understand it, your views use one of the properties on the singleton instance of the locator as their datacontext. The locator defines these various properties, and returns the proper viewmodel instance for each.
That's all fine, but I have trouble understanding how you actually populate these viewmodels with the model data the views are supposed to present.
For instance, suppose I have a view that presents a list of employees. I can create an EmployeesView and an EmployeesViewModel. In the ViewModelLocator, I can create a property that returns this EmployeesViewModel:
public EmployeesViewModel Employees
{
get
{
return ServiceLocator.Current.GetInstance<EmployeesViewModel>();
}
}
Now, the viewmodel needs a list of employees, so I can create some sort of dataservice that returns all employees, and register that with the Servicelocator in the ViewModelLocator's constructor:
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SimpleIoc.Default.Register<IDataService, AllEmployeesDataService>();
}
So, this'll work, if I instantiate the EmployeesView the EmployeesViewModel will be instantiated and injected with a dataservice that returns all employees.
However, now I want to see the details of a certain employee that I just clicked in the EmployeesView. This employee presumably has some sort of Id by which to retrieve him/her from a database, or whatever.
I can create an EmployeeDetailsView and an EmployeeDetailsViewModel, and add a property to the ViewModelLocator:
public EmployeeDetailsViewModel EmployeeDetails
{
return ServiceLocator.Current.GetInstance<EmployeeDetailsViewModel>();
}
and perhaps register some sort of dataservice in the ViewModelLocator's constructor:
SimpleIoc.Default.Register<IDataService, EmployeeDetailsDataService>();
But how do I tell either the dataservice or the viewmodel which employee they're supposed to present the details for? Where do I pass the employee id?
Am I looking at this all wrong? Anyone know of any good examples? All the examples I can find just return the same single instance of each viewmodel.
A simple example I use is to use a dataservice that gets injected into the constructor of the viewmodel, like you have. That data service returns an observable collection of my objects (in your case employees.) I create a listbox and Detail grid in the same view. So I bind the listbox to the observable collection that I can style and sort by using a collectionviewsource. For the details, I create a grid with the required fields that I want to display. I create a property in my viewmodel for the selected item (SelectedEmployee) of the listbox and bind them together. THen I bind the detail grid to the SelectedEmployee. THis will cause the fields to show the values from the selected employee.
Now you can us this for all CRUD operations and you can bind the slecteditemchanged event of the listbox to a relay command and add your business logic as needed. Another thing to note is you can break this apart to support async operations. I have another implementation where I take the selected item changed event of the listbox and execute a async get function to get the selcted item.
I hope this helps
I find J King answer quite good, but I will like to shed some more light and options never the less.
In the comment you asked about what happens if it's not the same view?
Here's an implementation I have:
<ListBox x:Name="YourListView"
ItemsSource="{Binding SomeCollection}"
SelectedItem="{Binding SelectedItemObject,
UpdateSourceTrigger=PropertyChanged}"
ToolTip="Double click to edit"
>
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Header ="Edit me" Command="{Binding Edit_Command}"
CommandParameter="{Binding SelectedItemObject}"
/>
<MenuItem Header ="Delete me" Command="{Binding Delete_Command}"
CommandParameter="{Binding SelectedItemObject}"
/>
</ContextMenu>
</ListBox.ContextMenu>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<Command:EventToCommand Command="{Binding Edit_Command}"
CommandParameter="{Binding ElementName=YourListView,
Path=SelectedItem}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
Notice that either double clicking on an object in that list, or right clicking and selecting one of the two options (namely edit and delete), will Call a command with a paramater.
Now, when you use the ContextMenu since you right clicked and object, it's selected, and you can just send it.
In the case of the double click, I'm using the name of the listbox to get the item.
From here what I'll do in the viewmodel is something like:
private void Execute_Edit(object param)
{
var your_object = (cast_to_your_type)param;
Messenger.Default.Send(new SwitchView(new SomeViewModel(_dataService,your_object)));
}
The ICommand will call Execute_Edit, which in turn will send a message using the Messenger.
This is how I defined SwitchView:
/// <summary>
/// Used as message, to switch the view to a different one.
/// </summary>
public class SwitchView
{
public SwitchView(MyViewModelBase viewmodel)
{
ViewModel = viewmodel;
}
public MyViewModelBase ViewModel { get; set; }
}
My MainWindow is registered to listen to those messages,and we know what to change to (obviously, to the view model given : SomeViewModel). The SwitchView on the main class will change the ViewModel property to the property the message passed.
Here's what I have in my main view:
<Border >
<ContentControl Content="{Binding Current_VM}" />
</Border>
So, whatever the Current_VM property is set to, will show you that view.
Hope that helped :)
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>
In our product, we use MVVM model first approach and it works nicely but with one caveat. When view becomes complex it takes time to create it from the data template. If the view is shown and hidden frequently, it becomes slightly irritating. If using view first, it would be easy enough to cache a view if needed - but when using DataTemplate and model first, we do not have much control of view creation.
Anybody solved this problem already without switching to the view first method?
Works beautifully if using the #blindmeis idea.
The overall recipe:
Create a ContentControl or UserControl named ViewCache:
public partial class ViewCache
{
public ViewCache()
{
InitializeComponent();
Unloaded += ViewCache_Unloaded;
}
void ViewCache_Unloaded(object sender, RoutedEventArgs e)
{
Content = null;
}
private Type _contentType;
public Type ContentType
{
get { return _contentType; }
set
{
_contentType = value;
Content = ViewFactory.View(value); // use you favorite factory
}
}
}
In the DataTemplate, use the ViewCache, pass the type of the real view you want to use:
<Window.Resources>
<DataTemplate DataType="{x:Type TestViewCache:Foo}">
<TestViewCache:ViewCache ContentType="{x:Type TestViewCache:View }"/>
</DataTemplate>
</Window.Resources>
<StackPanel>
<ContentPresenter Height="200" Width="300"
Content="{Binding ViewModel}"/>
<Button Content="Set VM" Click="SetVMClick"/>
<Button Content="UnSet VM" Click="UnSetVMClick"/>
</StackPanel>
with viewmodel first approach i think you have no chance to "cache" the view. so you may consider to use view first and a viewmodel locator for the heavyweight datatemplates workflows. here is a solution when using datatemplates with lists.
but maybe there is any solution with overriding the wpf datatemplate mechanism?
edit: what if you create just a "markerview" for your viewmodel, so wpf datatemplate can find it. and then within this marker view you create/rehydrate the real view? something like an view service locator?