I'm building my first WPF using MVVM pattern. With the help of this community, I manage to create my Model, my first ViewModel and view. Now I want to add some complexity to the app designing the basic application layout interface. My idea is to have at least 2 child views and one main view and separate them on several XAML:
Main.XAML
Products.XAML
Clients.XAML
Main will have a menu and a space to load child views (Products and Clients). Now following MVVM pattern all the navigation logic between views should be write on a ViewModel. So mi idea is to have 4 ViewModels:
MainViewModel
ProductsViewModel
ClientsViewModel
NavigationViewModel
So NavigationViewModel should contain a collection of child viewmodels? and an active viewmodel is that right?
So my questions are:
1) How can I load different views (Products, Clients) on Main view using MVVM pattern?
2) How do I implement navigation viewModel?
3) How can I control the max number of open or active views?
4) How can I switch between open views?
I have been doing a lot of search and reading and couldn't find any simple working example of MVVM navigation with WPF that loads multiple views inside a main view. Many of then:
1) Use external toolkit, which I don't want to use right now.
2) Put all the code for creating all the views in a single XAML file, which doesn't seems like a good idea because I need to implement near 80 views!
I'm in the right path here? Any help, especially with some code will be appreciated.
UPDATE
So, I build a test project following #LordTakkera advices, but get stuck. This is how my solution looks like:
I create:
Two Models (Clients and Products)
One MainWindow and two wpf user controls(Clients and Products) XAML.
Three ViewModels (Clients, Products and Main ViewModel)
Then I set dataContext on each view to corresponding viewModel. After that I create MainWindow with the ContentPresenter like this and bind it to a property of the viewmodel.
MainWindow.XAML
<Window x:Class="PruevaMVVMNavNew.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="519" Width="890">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="80"/>
<RowDefinition Height="*"/>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<Border Grid.Column="0" Grid.ColumnSpan="2" Background="AntiqueWhite" ></Border>
<Border Grid.Row="1" Grid.RowSpan="2" Background="AliceBlue"></Border>
<Border Grid.Row="1" Grid.Column="1" Background="CadetBlue"></Border>
<ContentPresenter Grid.Row="1" Grid.Column="1" x:Name="ContentArea" Content="{Binding CurrentView}"/>
<StackPanel Margin="5" Grid.Column="0" Grid.Row="1">
<Button>Clients</Button>
<Button>Products</Button>
</StackPanel>
</Grid>
And also this is viewmodel from MainWindow:
class Main_ViewModel : BaseViewModel
{
public Main_ViewModel()
{
CurrentView = new Clients();
}
private UserControl _currentView;
public UserControl CurrentView
{
get
{
return _currentView;
}
set
{
if (value != _currentView)
{
_currentView = value;
OnPropertyChanged("CurrentView");
}
}
}
}
So this load by default clients view and looks like this (which is just right!):
So I suppose I need a way to relate the buttons on the left, with a certain viemodel and then bind them with CurrentView Property of Main viewModel. How can I do that?
UPDATE2
According to #LordTakkera advice I modify my main viewModel this way:
class Main_ViewModel : BaseViewModel
{
public ICommand SwitchViewsCommand { get; private set; }
public Main_ViewModel()
{
//CurrentView = new Clients();
SwitchViewsCommand = new RelayCommand((parameter) => CurrentView = (UserControl)Activator.CreateInstance(parameter as Type));
}
private UserControl _currentView;
public UserControl CurrentView
{
get
{
return _currentView;
}
set
{
if (value != _currentView)
{
_currentView = value;
OnPropertyChanged("CurrentView");
}
}
}
}
I use RelayCommand instead of DelegateCommand but I think it works the same way. The command is executed when I hit the buttons and the type parameter string its ok but i get this error:
Translation: Value cannot be null. Parameter name: type. Suggestion use New keyword to create object instance
I don't know where to put the New keyword. I have try on CommandParameter but it wont work. Any idea? Thanks
UPDATE 3
After all the advices and help received here, and a lot of work, here is my final navigation menu and the base for my application interface.
I'm not sure you need a separate "navigation" view model, you could easily put it into the main. Either way:
To separate your "child" views, I would use a simple ContentPresenter on your "main" view:
<ContentPresenter Content="{Binding CurrentView}"/>
The easiest way to implement the backing property is to make it a UserControl, though some would argue that doing so violates MVVM (since the ViewModel is now dependent on a "View" class). You could make it an object, but you lose some type safety. Each view would be a UserControl in this case.
To switch between them, you are going to need some sort of selection control. I've done this with radio buttons before, you bind them like so:
<RadioButton Content="View 1" IsChecked="{Binding Path=CurrentView, Converter={StaticResource InstanceEqualsConverter}, ConverterParameter={x:Type views:View1}"/>
The converter is pretty simple, in "Convert" it just checks if the current control is a type of the parameter, in "ConvertBack" it returns a new instance of the parameter.
public class InstanceEqualsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return (parameter as Type).IsInstanceOfType(value);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return (bool)value ? Activator.CreateInstance(parameter as Type) : Binding.DoNothing;
}
}
Binding to a combobox or other selection control would follow a similar pattern.
Of course you could also use DataTemplates (with a selector, unfortunately not something I have done before) and load them into your resources using merged dictionaries (allowing separate XAML). I personally prefer the user control route, pick which is best for you!
This approach is "one view at a time". It would be relatively easy to convert to multiple views (your UserControl becomes a collection of user controls, use .Contains in the converter etc.).
To do this with buttons, I would use commands and take advantage of the CommandParameter.
The button XAML would look like:
<Button ... Command={Binding SwitchViewsCommand} CommandParameter={x:Type local:ClientsView}/>
Then you have a delegate command (tutorial here) that runs the activator code from the converter:
public ICommand SwitchViewsCommand {get; private set;}
public MainViewModel()
{
SwitchViewsCommand = new DelegateCommand((parameter) => CurrentView = Activator.CreateInstance(parameter as Type));
}
That is off the top of my head, but should be pretty close. Let me know how it goes!
Let me know if I provide any further information!
Update:
To answer your concerns:
Yes, each time you push the button a new instance of the view is created. You could easily fix this by holding a Dictionary<Type, UserControl> that has pre-created views and index into it. For that matter, you could use a Dictonary<String, UserControl> and use simple strings as the converter parameters. The disadvantage is that your ViewModel becomes tightly coupled to the kinds of views it can present (since it has to populate said Dictionary).
The class should get disposed, as long as no one else holds a reference to it (think event handlers that it registered for).
As you point out, only one view is created at a time so you shouldn't need to worry about memory. You are, of course, calling a constructor but that isn't THAT expensive, particularly on modern computers where we tend to have plenty of CPU time to spare. As always, the answer to performance questions is "Benchmark it" because only you have access to the intended deployment targets and entire source to see what actually performs the best.
IMHO the best choose for you is to use MVVM framework (PRISM, MMVM Light, Chinch, etc) because navigation is already implemented. If you want to create your own navigation - try DataTemplate.
Related
I am trying to learn MVVM and have come across a weird snag. I have a main menu with a drawer control that comes out and shows a menu:
In the main window where this drawer is, I have a ContentControl where I set its content with a Binding.
<ContentControl x:Name="MainWindowContentControl" Content="{Binding Path=WindowContent}"/>
This window's binding is set to a view model.
<Window.DataContext>
<viewmodels:MainWindowViewModel/>
</Window.DataContext>
and here is the ViewModel:
MainWindowViewModel.cs
public class MainWindowViewModel: ViewModelBase
{
private object _content;
public object WindowContent
{
get { return _content; }
set
{
_content = value;
RaisePropertyChanged(nameof(WindowContent));
}
}
public ICommand SetWindowContent { get; set; }
public MainWindowViewModel()
{
SetWindowContent = new ChangeWindowContentCommand(this);
}
}
So far up to this point, everything works fine. So for example, if I click "Recovery Operations", I get this:
RecoveryOperationsView.xaml
In "RecoveryOperationsView.xaml" (which is a UserControl) I also reference the view model from above like so..
<UserControl.DataContext>
<viewmodels:MainWindowViewModel/>
</UserControl.DataContext>
and have a button to call the command to change the Content property of the ContentControl from the main window..
<Button Grid.Row="2" Content="Restore Database" Width="150" Style="{StaticResource MaterialDesignFlatButton}" Command="{Binding SetWindowContent}" CommandParameter="DatabaseRecovery" >
In my class to process the commands, I change the content based off of the passed parameter using a switch statement like so
ChangeWindowContentCommand.cs
public class ChangeWindowContentCommand : ICommand
{
private MainWindowViewModel viewModel;
public ChangeWindowContentCommand(MainWindowViewModel vm)
{
this.viewModel = vm;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
switch (parameter)
{
case "Home":
viewModel.WindowContent = new HomeView();
break;
case "RecoveryOps":
viewModel.WindowContent = new RecoveryOperationsView();
break;
case "DatabaseRecovery":
viewModel.WindowContent = new DatabaseRestoreView();
break;
}
}
}
However, this is where I get lost... If I click something within this new window, say "Restore Database" and inspect it with a breakpoint, I can see the property being changed but the actual ContentControl Content property doesnt change to the new UserControl I made... I can change the content with anything in the drawer, but if I try to click a button in the hosted Content of the ContentControl nothing changes. What am I missing?
It's hard to be 100% sure without having your project to test with, but I am fairly confident that at least one of the issues is that your UserControl and your MainWindow use different instances of the MainWindowViewModel. You do not need to instantiate the VM for the user control, as it will inherit the DataContext from the MainWindow. The way it works in WPF is that if any given UIElement does not have theDataContext assigned explicitly, it will inherit it from the first element up the logical tree that does has one assigned.
So, just delete this code, and it should solve at least that issue.
<UserControl.DataContext>
<viewmodels:MainWindowViewModel/>
</UserControl.DataContext>
And since you're learning WPF, I feel obligated to provide a couple other tips. Even though you're using a ViewModel, you are still mixing UI and logic by creating a very specific implementation of ICommand and assigning a UI element through your ViewModel. This breaks the MVVM pattern. I know MVVM takes a little time to understand, but once you do, it is very easy to use and maintain.
To solve your problem, I would suggest creating View Models for each of your user controls. Please see this answer, where I go into quite a bit of detail on the implementation.
For switching the different views, you have a couple of options. You can either use a TabControl, or if you want to use a command, you can have a single ContentControl bound to a property of MainWindowViewModel that is of type ViewModelBase. Let's call it CurrentViewModel. Then when the command fires, you assign the view model of the desired user control to that bound property. You will also need to utilize implicit data templates. The basic idea is that you create a template for each of the user control VM types, which would just contains an instance of the Views. When you assign the user control VM to the CurrentViewModel property, the binding will find those data templates and render the user control. For example:
<Window.Resources>
<DataTemplate DataType = "{x:Type viewmodels:RecoveryOperationsViewModel}">
<views:RecoveryOperationsView/>
</DataTemplate>
<!-- Now add a template for each of the views-->
</Window.Resources>
<ContentControl x:Name="MainWindowContentControl" Content="{Binding CurrentViewModel}"/>
See how this approach keeps UI and logic at an arm's length?
And lastly, consider creating a very generic implementation of ICommand to use in all your ViewModels rather than many specific implementations. I think most WPF programmers have more or less this exact RelayCommand implementation in their arsenal.
I have created a composite user control containing a Toolbar and a Datagrid, and expose them as public properties. Is there a way to add new button to Toolbar and set a DataTemplate for Datagrid in XAML, instead of implementing them in the code-behind file if I use this user control in another Window or user control?
I found a similar link here, but has no idea how to do it. Please help.
Here is the Xaml:
<UserControl x:Class="CRUDDataGrid1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" >
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ToolBarTray Grid.Row="0" >
<ToolBar x:Name="tb">
<Button x:Name="Add" Content="Add">
</Button>
</ToolBar>
</ToolBarTray>
<DataGrid Grid.Row="1" x:Name="dg">
</DataGrid>
</Grid>
</UserControl>
And here is the code-behind:
public partial class CRUDDataGrid1 : UserControl
{
public ToolBar ToolBar { get; set; }
public DataGrid DataGrid { get; set; }
public ObservableCollection<DataGridColumn> Columns { get; private set; } //edited
public CRUDDataGrid1()
{
InitializeComponent();
ToolBar = tb;
DataGrid = dg;
Columns = dg.Columns; //edited
}
}
And I want to use this user control in another user control like this:
<UserControl x:Class="UserControl1" ...>
<Grid>
<local:CRUDDataGrid1>
<local:CRUDDataGrid1.ToolBar>
<Button x:Name="Delete" Content="Delete">
</Button>
</local:CRUDDataGrid1.ToolBar>
<local:CRUDDataGrid1.DataGrid ItemsSource="{Binding Customers}">
<local:CRUDDataGrid1.Columns>
<DataGridTextColumn Header="First Name" Binding="{Binding XPath=#FirstName}" />
<DataGridTextColumn Header="Last Name" Binding="{Binding XPath=#LastName}" />
<local:CRUDDataGrid1.Columns>
</local:CRUDDataGrid1.DataGrid>
</local:CRUDDataGrid1>
</Grid>
</UserControl>
1 Foreword
Having a child control which owns a ToolBar and wanting a parent of that child control to add toolbar items to the ToolBar owned by the child is a tell-tale sign of bad
design. The primary and most important advice for you is to rethink your software design to avoid this kind of shared/split initialization.
In almost any scenario, you want the toolbar owned by the top-most control such as the main window, or a document window (in case your application has MDI or floating windows).
The toolbar items would be gathered from the respective controls housed within that window; for example, copy/paste/etc. actions from the document editor control, actions for creating or loading a new document from somewhere else, etc.
Side note: Often, such a design happens because novice WPF programmers want to realize button actions in the old-fashioned way of using Click event handlers. Such Click-event
handlers create code dependencies, and as long as they can be contained within just one (custom) control everything is fine. However, as soon as this is not feasible any more
(for example when an action should appear as a toolbar button or the same action should be triggered through a menu), trying to stick with Click event handlers will lead to convoluted code even for simple UIs and can cause severe headache...
The mechanism in WPF to avoid those pesky Click event handlers are Commands, or more specifically RoutedCommands. To be fair, it has to be noted that RoutedCommands have their own
share of challenges. However, many fine folks wrote many interesting and important things about using WPF's RoutedCommands and how to expand beyond their functionality, so the
only sane advice i can give here is to use the powers of Google if you want/need to know more.
2 Answering the question, but not solving the underlying design issue
To create a ToolBar which has collections of toolbar items defined at different places, while using multiple toolbar bands in the same ToolBarTray is not desired, the toolbar item collections need to be merged into a single list at some point. This can either be done somehow in code-behind, or it can be done in XAML with the help of a custom IMultiValueConverter.
The custom IMultiValueConverter - let's call it MergeCollectionsConverter - will be
agnostic to any data type. It just takes a number of IEnumerables and adds all their elements to the result list. It even accepts objects which are not IEnumerable, those objects themselves will be added to the result list.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows.Data;
using System.Windows.Documents;
namespace MyStuff
{
public class MergeCollectionsConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values == null) return null;
List<object> combinedList = new List<object>();
foreach (object o in values)
{
if (o is IEnumerable)
combinedList.AddRange( ((IEnumerable) o).Cast<object>() );
else
combinedList.Add(o);
}
return combinedList;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
I assume furthermore that the ToolBar inside of CRUDDataGrid1 should be fed from two toolbar item collections. The first collection with the default toolbar items is defined within CRUDDataGrid1. The second collection should allow other controls to append additional toolbar items after the default items; this collection therefore has to be publicly accessible.
Based on your example code from the question, your CRUDDataGrid1 class could look like the following (just considering the toolbar, it does not represent the complete class by any means):
CRUDDataGrid1.cs:
public partial class CRUDDataGrid1 : UserControl, INotifyPropertyChanged
{
public ObservableCollection<object> AdditionalToolbarItems { get { return _additionalToolbarItems; } }
private readonly ObservableCollection<object> _additionalToolbarItems = new ObservableCollection<object>();
public event PropertyChangedEventHandler PropertyChanged;
public UserControl1()
{
InitializeComponent();
_additionalToolbarItems.CollectionChanged +=
(sender, eventArgs) =>
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs("AdditionalToolbarItems"));
};
...other constructor code...
}
}
CRUDDataGrid1.xaml:
<DockPanel>
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<ToolBar.Resources>
<DataTemplate DataType="{x:Type My:UseCommand}">
<Button
Style="{StaticResource {x:Static ToolBar.ButtonStyleKey}}"
Command="{Binding Command}"
CommandTarget="{Binding Target}"
CommandParameter="{Binding Parameter}"
Content="{Binding Command.Text}"
/>
</DataTemplate>
<My:MergeCollectionsConverter x:Key="convToolbarItems" />
<x:Array x:Key="defaultToolbarItems" Type="{x:Type sys:Object}">
<My:UseCommand Command="ApplicationCommands.New" />
<My:UseCommand Command="ApplicationCommands.Cut" />
<My:UseCommand Command="ApplicationCommands.Paste" />
</x:Array>
</ToolBar.Resources>
<ToolBar.ItemsSource>
<MultiBinding Converter="{StaticResource convToolbarItems}">
<Binding Source="{StaticResource defaultToolbarItems}" />
<Binding Path="AdditionalToolbarItems" ElementName="crudDataGrid1" />
</MultiBinding>
</ToolBar.ItemsSource>
</ToolBar>
</ToolBarTray>
<DataGrid x:Name="dg" />
</DockPanel>
The first collection is a 'static resource' in the ToolBar's resource directory,
identified by the resource key "defaultToolbarItems". The second is the collection provided by CRUDDataGrid1's property AdditionalToolbarItems. Using a <MultiBinding>
with the aforementioned converter, the merged list is bound to the ToolBar's ItemsSource.
Looking at the C# source code of the AdditionalToolbarItems property, you will notice the INotifyPropertyChanged implementation and the handler for the CollectionChanged
event. Why is that? Remember, that AdditionalToolbarItems is a read-only property. At the time the CRUDDataGrid1 control has been fully constructed, the data binding has been
set and AdditionalToolbarItems been processed by the multi binding. And it would never be
processed again, since the property itself will never change its value (it will
always refer to the same ObservableCollection). To make the <MultiBinding> re-evaluate the bound properties whenever the content of the AdditionalToolbarItems collection has
changed, the code needs to listen for CollectionChanged events and fire an
PropertyChanged event whenever the content of AdditionalToolbarItems has changed, which in turn will cause the <MultiBinding> to re-evaluate the bound properties.
You will also note the usage of <My:UseCommand> elements instead of using <Button>. Well, you could use <Button>, and it would work as well. Until your application wants to use multiple ToolBars at once sharing the same default buttons - in which case you have a problem: A button is a control and thus has one parent UI element. You cannot share
a button control amongst several toolbars, because a control can only be owned as a child by one parent UI element. Thus, RoutedCommands are used instead of button controls
(another, equally important reason will become obvious if you read the 'real' solution in section 3 below). Still, technically nothing would stop you from declaring <Button>
elements -- you could even mix <My:UseCommand> with <Button> (and other elements, as long as those can be rendered in the toolbar).
UseCommand is a pretty small and simple class which allows you to tell which command to use (plus optional CommandTarget and CommandParameter, if required):
namespace MyStuff
{
public class UseCommand
{
public System.Windows.Input.ICommand Command { get; set; }
public System.Windows.IInputElement Target { get; set; }
public object Parameter { get; set; }
}
}
The ToolBar will need a DataTemplate to properly display the command and its parameters stored inside UseCommand. You can see this DataTemplate as part of the ToolBar
resource dictionary in the XAML code above.
With these things in place, using CRUDDataGrid1 in UserControl1 and adding additional toolbar items could look like this:
<UserControl x:Class="UserControl1" ...>
<Grid>
<local:CRUDDataGrid1>
<local:CRUDDataGrid1.AdditionalToolbarItems x:Name="cdg">
<My:UseCommand Command="ApplicationCommands.Close" CommandTarget="{Binding ElementName=cdg}" />
<My:UseCommand Command="ApplicationCommands.New" CommandTarget="{Binding ElementName=cdg}" />
</local:CRUDDataGrid1.AdditionalToolbarItems>
...
</local:CRUDDataGrid1.DataGrid>
</Grid>
</UserControl>
For my example code, i used commands provided by System.Windows.Input.ApplicationCommands. You can ofcourse roll your own commands (as we will see below). Also note the demonstrated usage of the CommandTarget property. Whether using this property is necessary requires some understanding of how RoutedCommands work and mostly depends on where which element in the UI's visual/logical tree has established handlers for that particular command.
3 Using RoutedCommands to solve the design issue and the question
Having read section 2, you should already gotten the idea that RoutedCommands will help you to separate the provision of user-invokable actions by whatever component from the actual UI representation, and that this can help you avoiding the shenanigans about the somewhat convoluted composition of the ToolBar from different sources. Because, all that CRUDDataGrid1 essentially needs to provide for your GUI are the commands for a toolbar (or a menu, or any other command invokers for that matter).
From what i can glance from your source code, CRUDDataGrid1 is responsible for executing the "Add" action, whereas UserControl1 is responsible the "Delete" action.
Both actions should appear in the same toolbar.
Let's look at the "Add" action of CRUDDataGrid1. First and foremost, to make this action invokable through a RoutedCommand, an appropriate RoutedCommand object needs to be provided, obviously. You might choose one of the RoutedCommands provided by .NET (as declared in ApplicationCommands, ComponentCommand and NavigationCommand).
However, this is not always a good idea. Common commands such as ApplicationCommands.Copy can be executed by pretty much any control which supports clipboard operations, and knowing which actual control will handle the invocation of such a command requires knowing about how RoutedCommands are routed through the visual tree and how the logcial focus affects this routing. Thus, sometimes it is
easier to define your own RoutedCommand as a public static property - which we will do here for the "Add" action:
public partial class CRUDDataGrid1 : UserControl
{
public static readonly RoutedCommand AddCommand = new RoutedCommand("CRUDDataGridCommand.Add", typeof(CRUDDataGrid1));
public UserControl1()
{
InitializeComponent();
CommandBindings.Add(
new CommandBinding(
AddCommand,
OnExecutedAddCommand,
CanExecuteAddCommand
)
);
...other constructor code...
}
private void CanExecuteAddCommand(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = ...here your code that decides whether the "Add" command can execute
(and thus whether any button which uses this command will be enabled/disabled)
}
private void CanExecuteAddCommand(object sender, ExecutedRoutedEventArgs e)
{
...execute the "Add" action here...
}
}
Note the command-binding in the constructor as well as the respective methods handling the command. Just to avoid confusion: It is not required that the object serving as CommandTarget has to implement command-bindings. CommandTarget merely specifies the object in the visual/logical tree at which the routing starts.
While i do not show it here, the implementation regarding the DeleteCommand in UserControl is following the same pattern.
public partial class UserControl1 : UserControl
{
public static readonly RoutedCommand DeleteCommand = new RoutedCommand("UserControl1Command.Delete", typeof(UserControl1));
...same implementation approach as demonstrated for CRUDDataGrid1.AddCommand...
}
Creating the ToolBar can now happen entirely in UserControl1.xaml without worrying how the respective actions represented by the commands are executed. Note, that it is fine to use <Button> since the toolbar is entirely created in UserControl1 without a possibility that any of these buttons could be "shared" with another control. Also, note the absence of those helper classes like UseCommand and MergeCollectionsConverter which were required for the somewhat convoluted scenario in section 2 of my answer.
<UserControl x:Class="UserControl1" ...>
<DockPanel>
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<Button Content="Add" Command="{x:Static local:CRUDDataGrid1.AddCommand}" CommandTarget="{Binding ElementName=cdg}" />
<Button Content="Delete" Command="{x:Static local:UserControl1.DeleteCommand}" />
</ToolBar>
</ToolBarTray>
<local:CRUDDataGrid1 x:Name="cdg" ItemsSource="{Binding Customers}">
<local:CRUDDataGrid1.Columns>
<DataGridTextColumn Header="First Name" Binding="{Binding XPath=#FirstName}" />
<DataGridTextColumn Header="Last Name" Binding="{Binding XPath=#LastName}" />
</local:CRUDDataGrid1.Columns>
</local:CRUDDataGrid1>
</DockPanel>
</UserControl>
CRUDataGrid1 should probably directly inherit from the DataGrid type (not being a UserControl), implementing the extended CRUD functionality as you require.
By letting CRUDataGrid1 provide only RoutedCommands for any desired user-action, you and anybody else in your team is free in the decision where in the GUI to use the RoutedCommands - in tool bars, in menus or whereever else. You can use multiple buttons using the same command - no problem there. The infrastructure behind RoutedCommands also will take care about automatically enabling/disabling such buttons depending on the result of the CanExecute method bound to a command.
In the example given here, i did let CRUDataGrid1 and UserControl1 provide the RoutedCommands. But if you have many commands and more complex software, then there is nothing speaking against having a central place for defining those commands (similar to what Microsoft did with the RoutedCommands provided by the .NET framework).
So, I have made my own subclass of UserControl, called ChildView (I really can't come up with a decent name), that I want to show inside a container in a window, I have many different kinds of these UserControls and the window must be capable of showing all of them. The UserControls have implemented my subclass like this:
<src:ChildView x:Class="(namespace).LoginView" [...]>
public partial class LoginView : ChildView
And I have tried to add it to my window like so:
<Grid x:Name="ViewHolder" Grid.Column="1" Grid.Row="1">
<src:ChildView DataContext="{Binding CurrentView}" />
</Grid>
private ChildView _currentView;
public ChildView CurrentView
{
get { return _currentView; }
set
{
if (_currentView == value)
return;
_currentView = value;
smLog.Trace("View set to {0}", value.GetType().Name);
NotifyPropertyChanged("CurrentView");
}
}
However, this does not work. Nothing is shown in my container when I set CurrentView. There are no error messages in the output that would indicate a problem with the binding. Other data bindings in the window works. I can use my ChildViews by specifying their classes directly in the XAML, i.e:
<Grid x:Name="ViewHolder" Grid.Column="1" Grid.Row="1">
<src:LoginView />
</Grid>
I've read some about dependency properties but I don't think I need one here? I did try to implement one anyway but it didn't seem to help, though I probably made some mistake, I couldn't quite wrap my head around it...
So I guess my question is; do I need a dependency property? If so, how do I implement it in this case? If not, what is the problem?
Changing the Child's DataContext won't matter, you're trying to change the control itself, not the data it's bound to. What you need to do is add a placeholder control that would contain the actual view. WPF has such a thing built in, take a look at ContentControl.
Change your grid so it'll containt a ContentControl instead of ChildView, and bind the view to the control's Content property
<Grid>
<ContentControl Content="{Binding CurrentView}"/>
</Grid>
I'm trying to bind a ContentControl's Content to a UserControl I have instantiated in my ViewModel. I cannot use the method with binding to a ViewModel and then have the UserControl be the DataTemplate of the ViewModel, as I need the Content of the ContentControl to be able to change frequently, using the same instance of the UserControls/Views, and not instantiate the views each time i re-bind.
However, when setting the UserControl-property to a UserControl-instance, and then when the view is rendered/data-bound I get: Must disconnect specified child from current parent Visual before attaching to new parent Visual. Even though I have not previously added this UserControl to anywhere, I just created this instance earlier and kept it in memory.
Are there a better way to achieve what I am doing?
In the ViewModel
public class MyViewModel : INotifyPropertyChanged
{
//...
private void LoadApps()
{
var instances = new List<UserControl>
{
new Instance1View(),
new Instance2View(),
new Instance3View(),
};
SwitchInstances(instances);
}
private void SwitchInstances(List<UserControl> instances)
{
CenterApp = instances[0];
}
//...
private UserControl _centerApp;
public UserControl CenterApp
{
get { return _centerApp; }
set
{
if (_centerApp == value)
{
return;
}
_centerApp = value;
OnPropertyChanged("CenterApp");
}
}
//...
}
In the View.xaml
<ContentControl Content="{Binding CenterApp}"></ContentControl>
Too long for a comment.
Building up on what #Kent stated in your comment, The whole point of MVVM is to disconnect the view-model from view related stuff(controls) which blocks the testing capability of GUI applications. Thus you having a UserControl / Button / whatever graphical view-related item negates the entire principle of MVVM.
You should if using MVVM comply with its standards and then re-address your problem.
With MVVM you normally have 1 view <-> 1 view-model
View knows about its View Model(Normally through DataContext). Reverse should not be coded into.
You try to put logic controlling the view in the view-model to allow testing logic(Commands and INPC properties)
... and quite a few more. It's pretty specific in the extents of view-model not having view related stuff for eg not even having properties in view-model like Visibility. You normally hold a bool and then in the view use a converter to switch it to the Visibility object.
Reading up a bit more into MVVM would certainly help you,
Now for something to address your current issue:
Following a MVVM structure,
your going to have ViewModels such as
Main: MyViewModel
Derive all instance ViewModels from a base to allow them being kept in a list.
List or hold individually Instance1ViewModel, Instance2ViewModel, Instance3ViewModel in MyViewModel (Either create it yourself or if your using an IOC container let it inject it)
Have MyViewModel expose a property just like your posted example:
Example:
// ViewModelBase is the base class for all instance View Models
private ViewModelBase _currentFrame;
public ViewModelBase CurrentFrame {
get {
return _currentFrame;
}
private set {
if (value == _currentFrame)
return;
_currentFrame = value;
OnPropertyChanged(() => CurrentFrame);
}
}
Now in your MyView.xaml View file you should(does'nt have to be top level) set the top-level DataContext to your MyViewModel
Your View's xaml can then be declared like:
Example:
...
<Window.Resources>
<DataTemplate DataType="{x:Type local:Instance1ViewModel}">
<local:Instance1View />
</DataTemplate>
<DataTemplate DataType="{x:Type local:Instance2ViewModel}">
<local:Instance2View />
</DataTemplate>
<DataTemplate DataType="{x:Type local:Instance3ViewModel}">
<local:Instance3View />
</DataTemplate>
</Window.Resources>
<Grid>
<ContentControl Content="{Binding Path=CurrentFrame}" />
</Grid>
...
Thats it!. Now you just switch the CurrentFrame property in your view-model and make it point to any of three instance view-models and the view will be correspondingly updated.
This gets you an MVVM compliant application, for your other issue of working around not having to recreate views dynamically based on DataTemplate you could follow the approaches suggested here and expand it for your own usage.
I'm trying to choose the best way to implement this UI in MVVM manner. I'm new to WPF (like 2 month's) but I have huge WinForms experience.
The ListBox here act's like a TabControl (so it switches the view to the right), and contains basically the Type of item's displayed in tables. All UI is dynamic (ListBox items, TabItems and Columns are determined during run-time). The application is targeting WPF and Silverlight.
Classes we need for ViewModel:
public abstract class ViewModel : INotifyPropertyChanged {}
public abstract class ContainerViewModel : ViewModel
{
public IList<ViewModel> Workspaces {get;set;}
public ViewModel ActiveWorkspace {get;set;}
}
public class ListViewModel<TItem> where TItem : class
{
public IList<TItem> ItemList { get; set; }
public TItem ActiveItem { get; set; }
public IList<TItem> SelectedItems { get; set; }
}
public class TableViewModel<TItem> : ListViewModel<TItem> where TItem : class
{
public Ilist<ColumnDescription> ColumnList { get; set; }
}
Now the question is how to wire this to View.
There are 2 base approaches I can see here:
With XAML: due to lack of Generics support in XAML, I will lose strong typing.
Without XAML: I can reuse same ListView<T> : UserControl.
Next, how to wire data, I see 3 methods here (with XAML or without doesn't matter here). As there is no simple DataBinding to DataGrid's Columns or TabControl's TabItems the methods I see, are:
Use DataBinding with IValueConverter: I think this will not work with WPF|Silverlight out of the box control's, as some properties I need are read-only or unbindable in duplex way. (I'm not sure about this, but I feel like it will not work).
Use manual logic by subscribing to INotifyPropertyChanged in View: ViewModel.PropertyChanged+= ....ViewModel.ColumnList.CollectionChanged+= ....
Use custom controll's that support this binding: Code by myself or find 3d party controls that support this binding's (I don't like this option, my WPF skill is too low to code this myself, and I doubt I will find free controls)
Update: 28.02.2011
Things get worser and worser, I decided to use TreeView instead of ListBox, and it was a nightmare. As you probably guess TreeView.SelectedItems is a readonly property so no data binding for it. Ummm all right, let's do it the old way and subscribe to event's to sync view with viewmodel. At this point a suddenly discovered that DisplayMemberPath does nothing for TreeView (ummmm all right let's make it old way ToString()). Then in View's method I try to sync ViewModel.SelectedItem with TreeView's:
private void UpdateTreeViewSelectedItem()
{
//uiCategorySelector.SelectedItem = ReadOnly....
//((TreeViewItem) uiCategorySelector.Items[uiCategorySelector.Items.IndexOf(Model.ActiveCategory)]).IsSelected = true;
// Will not work Items's are not TreeViewItem but Category object......
//((TreeViewItem) uiCategorySelector.ItemContainerGenerator.ContainerFromItem(Model.ActiveCategory)).IsSelected = true;
//Doesn't work too.... NULL // Changind DataContext=Model and Model = new MainViewModel line order doesn't matter.
//Allright.. figure this out later...
}
And none of methods I was able to think of worked....
And here is the link to my sample project demonstrating Control Library Hell with MVVM: http://cid-b73623db14413608.office.live.com/self.aspx/.Public/MVVMDemo.zip
Maciek's answer is actually even more complicated than it needs to be. You don't need template selectors at all. To create a heterogeneous tab control:
Create a view model class for each type of view that you want to appear as tab items. Make sure each class implements a Text property that contains the text that you want to appear in the tab for its item.
Create a DataTemplate for each view model class, with DataType set to the class's type, and put the template in the resource dictionary.
Populate a collection with instances of your view models.
Create a TabControl and bind its ItemsSource to this collection, and add an ItemTemplate that displays the Text property for each item.
Here's an example that doesn't use view models (and that doesn't implement a Text property either, because the objects I'm binding to are simple CLR types), but shows how template selection works in this context:
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:coll="clr-namespace:System.Collections;assembly=mscorlib">
<DockPanel>
<DockPanel.Resources>
<coll:ArrayList x:Key="Data">
<sys:String>This is a string.</sys:String>
<sys:Int32>12345</sys:Int32>
<sys:Decimal>23456.78</sys:Decimal>
</coll:ArrayList>
<DataTemplate DataType="{x:Type sys:String}">
<TextBlock Text="{Binding}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type sys:Int32}">
<StackPanel Orientation="Horizontal">
<TextBlock>This is an Int32:</TextBlock>
<TextBlock Text="{Binding}"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type sys:Decimal}">
<StackPanel Orientation="Horizontal">
<TextBlock>This is a Decimal: </TextBlock>
<TextBlock Text="{Binding}"/>
</StackPanel>
</DataTemplate>
</DockPanel.Resources>
<TabControl ItemsSource="{StaticResource Data}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
</DockPanel>
</Page>
Of course in a real MVVM application those DataTemplates would use UserControls to map each type to its view:
<DataTemplate DataType="{x:Type my:ViewModel}">
<my:View DataContext="{Binding}"/>
</DataTemplate>
Maciek and Robert already gave you some ideas on how to implement this.
For the specifics of binding the columns of the DataGrid I strongly recommend Meleak's answer to that question.
Similar to that you can use attached properties (or Behaviors) and still maintain a clean ViewModel in MVVM.
I know the learning curve for WPF is quite steep and you're struggling already. I also know that the following suggestion doesn't help that and even makes that curve steeper. But your scenario is complex enough that I'd recommend to use PRISM.
I wrote an article and a sample application with source code available, where I discuss and show the problems I have mentioned here and how to solve them.
http://alexburtsev.wordpress.com/2011/03/05/mvvm-pattern-in-silverlight-and-wpf/
In order to connect your ViewModel to your View you need to assign the View's DataContext. This is normally done in the View's Constructor.
public View()
{
DataContext = new ViewModel();
}
If you'd like to see your view model's effect at design time, you need to declare it in XAML, in the View's resources, assign a key to it, and then set the target's DataContext via a StaticResource.
<UserControl
xmlns:vm="clr-namespace:MyViewModels
>
<UserControl.Resources>
<vm:MyViewModel x:Key="MyVM"/>
</UserControl.Resources>
<MyControl DataContext={StaticResource MyVM}/>
</UserControl>
(The above is to demonstrate the design-time trick works)
Since you're dealing with a scenario that includes a container such as the TabControl I'd advocate considering the following things :
Hold your child ViewModels in a Property of type ObservableCollection
Bind the TabControls ItemsSource to that property
Create a new View that derives from TabItem
Use a template selector to automatically pick the type of the view based on the type of the view model.
Add IDisposable to yoour child ViewModels and create functionality to close the views.
Hope that helps a bit, if you have further questions let me know.