MVVM WPF change window and close the previous - c#

As a starter in this all architecture of MVVM I'm having a few doubts regarding the navigation between one window to another. I'm using the Framework MVVM Light.
The behavior I expect is in WinForms like this:
GeneralWindow gw = new GeneralWindow(); this.Hide(); // or close
gw.Show();
I already lose a couple of hours trying to find some hints using the messenger, but the methods I found I have to use code-behind in the view and that is not very MVVMish.
Best regards and thank you in advance.

The behavior I expect is in WinForms like this:
GeneralWindow gw = new GeneralWindow(); this.Hide(); // or close gw.Show();
MVVM pattern divides View from ViewModel. So it is not eligible to create new View from ViewModel. Creating window instance and showing window from view model is violation of MVVM". So I suggest you to use the popular technique where you can change Views using ContentControl and DataTemplate.
Let's dive in this technique:
<Window>
<Window.Resources>
<DataTemplate DataType="{x:Type ViewModelA}">
<localControls:ViewAUserControl/>
</DataTemplate>
<DataTemplate DataType="{x:Type ViewModelB}">
<localControls:ViewBUserControl/>
</DataTemplate>
<Window.Resources>
<ContentPresenter Content="{Binding CurrentView}"/>
</Window>
If Window.DataContext is an instance of ViewModelA, then ViewA will be displayed and Window.DataContext is an instance of ViewModelB, then ViewB will be displayed.
Let me show an example where it can be seen where you should put DataTemplates:
<Window x:Class="SimpleMVVMExample.ApplicationView"
...The code omitted for the brevity...
Title="Simple MVVM Example with Navigation" Height="350" Width="525">
<Window.Resources>
<DataTemplate DataType="{x:Type ViewModelA}">
<localControls:ViewAUserControl/>
</DataTemplate>
<DataTemplate DataType="{x:Type ViewModelB}">
<localControls:ViewBUserControl/>
</DataTemplate>
</Window.Resources>
<DockPanel>
<Border DockPanel.Dock="Left" BorderBrush="Black" BorderThickness="0,0,1,0">
<ItemsControl ItemsSource="{Binding ListOfViewModels}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Name}"
Command="{Binding DataContext.ChangePageCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
CommandParameter="{Binding }"
Margin="2,5"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<ContentControl Content="{Binding CurrentDataTemplateViewModel}" />
</DockPanel>
</Window>
The best example I've ever seen and read it is made by Rachel Lim. See the example.
Update:
If you want really open new Window, then you should create an intermediate layer to make ViewModel not dependent on a concrete implementation of creating new window.
public class YourViewModel
{
private readonly IWindowFactory windowFactory;
private ICommand openNewWindow;
public YourViewModel(IWindowFactory _windowFactory)
{
windowFactory = windowFactory;
/**
* Would need to assign value to m_openNewWindow here, and
* associate the DoOpenWindow method
* to the execution of the command.
* */
openNewWindow = null;
}
public void DoOpenNewWindow()
{
windowFactory.CreateNewWindow();
}
public ICommand OpenNewWindow { get { return openNewWindow; } }
}
public interface IWindowFactory
{
void CreateNewWindow();
}
public class ProductionWindowFactory: IWindowFactory
{
#region Implementation of INewWindowFactory
public void CreateNewWindow()
{
NewWindow window = new NewWindow
{
DataContext = new NewWindowViewModel()
};
window.Show();
}
#endregion
}
How to close a Window?
There are a lot of approaches.. One of them is:
Application.Current.MainWindow.Close()

Related

Open a second usercontrol from the first user control wpf MVVM

I have a WPF application with MVVM pattern. I have a Main window with a few tabs. On clicking different buttons in the main window, it's user control will show up. I have that working fine. However, I need to open a user control when a button is clicked in one of the usercontrols. Not from the Main Window. How do I do this?
This is what I have so far:
MainWindow.XAML : I am creating data templates for each user control
<DataTemplate DataType="{x:Type homeViewModel:HomeWindowViewModel}">
<homeViewModel:HomeWindow></homeViewModel:HomeWindow>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModel:PauseViewModel}">
<viewModel:Pause></viewModel:Pause>
</DataTemplate>
<DataTemplate DataType="{x:Type logoffVM:LogOffViewModel}">
<logoffVM:LogOff></logoffVM:LogOff>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModel:ReportViewModel}">
<viewModel:Report></viewModel:Report>
</DataTemplate>
And have the usercontrols showing up inside ItemsControl:
<Grid x:Name="UserControlGrid" Width="Auto" Height="auto" Margin="100,40,0,0">
<ItemsControl ItemsSource="{Binding ViewsToShow}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid IsItemsHost="True" Width="auto" Height="auto"></Grid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
I have the buttons bound to a Command in the MainWindow ViewModel class.
In the MainWindowViewModel:
public ObservableCollection<ObservableObject> ViewsToShow
{
get
{
return viewsToShow;
}
set
{
viewsToShow = value;
OnPropertyChanged("ViewsToShow");
}
}
The Commands :
public ICommand GetNextCommand
{
get { return new RelayCommand(() => GetNext()); }
}
public ICommand GetOrderCommand
{
get { return new RelayCommand(() => GetOrder()); }
}
And the Methods that load usercontrols:
public void GetNext()
{
Order orders = new Order();
ViewsToShow.Clear();
ViewsToShow.Add(new OrderDisplayViewModel("GetNext", orders, new WindowFactory()));
}
public void GetOrder()
{
ViewsToShow.Clear();
ViewsToShow.Add(new GetOrderViewModel());
}
All this is working fine from the MainWindow. But how do I open the GetOrder user control from the GetNext user control?
Any ideas?
You could bind to a property of the parent window's DataContext from a UserControl using a RelativeSource;
<Button Command="{Binding DataContext.GetOrderCommand, RelativeSource={RelativeSource AncestorType=Window}}" />

Add TabItems to an existing TabControl WPF/MVVM

I have TabControl that has already define some TabItems on XAML. I need to create new TabItems and add to it.
If I use ItemSource I get an exception Items collection must be empty before using ItemsSource.
The solution I have found so far is to create those TabItems I have already defined on XAML but programmatically on the ViewModel, so I can created the others I really need, but doesn't seems to be a good solution.
Other solution would be to add the TabControl as a property and use the Code-Behind to bind it to the ViewModel, which I would like to avoid.
So, I'm just wondering if there is a way to do this only with XAML and MVVM.
Edit:
ItemSource attempt, which is working.
XAML:
<TabControl Grid.Row="1"
Grid.Column="0"
VerticalAlignment="Stretch"
BorderThickness="0.5"
BorderBrush="Black"
ItemsSource="{Binding Model.TabItems, Mode=TwoWay}">
<!--<TabControl.Items>
</TabControl.Items>-->
</TabControl>
Model
public ObservableCollection<TabItem> TabItems {get;set;}
VM
TabItem tabItem = new TabItem { Content = new DetailedViewModel((MyObject)inCommandParameter) };
Model.TabItems.Add(tabItem);
What you are doing here is NOT MvvM. Idea behind it is to keep parts of the app separate, i.e. Model should NOT return any UI elements. If you want to use this with any other UI framework for example WinForms then it will fail and will require additional work.
What you need is something like this, bear in mind that this is an example and you will need to modify this to comply with your requirements.
Model class:
namespace Model
{
public class Profile
{
public string Name { get; set; }
public static int I { get; set; } = 2;
}
}
After this you will need the ViewModel:
namespace VM
{
public class MainViewModel : BaseViewModel
{
public MainViewModel()
{
ProfilesCollection = new List<Profile>();
for (int i = 0; i < 100; i++)
{
ProfilesCollection.Add(new Profile() {Name = $"Name {i}"});
}
}
private List<Profile> profilesCollection;
public List<Profile> ProfilesCollection
{
get { return profilesCollection; }
set { profilesCollection = value; OnPropertyChanged(); }
}
}
}
Now we have base to work with. After that I assume you know how to add the relevant references in your xaml, but this might be seen by other people so I will include it anyway.
Here is a complete MainWindow.xaml:
<Window x:Class="SO_app.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:VM;assembly=VM"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:converter="clr-namespace:SO_app.Converters"
xmlns:validation="clr-namespace:SO_app.Validation"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
xmlns:local="clr-namespace:SO_app"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:model="clr-namespace:Model;assembly=Model"//reference to my model
mc:Ignorable="d"
Title="MainWindow" Height="452.762" Width="525" Closing="Window_Closing">
<!-- d:DataContext="{d:DesignInstance Type=vm:MainViewModel, IsDesignTimeCreatable=True}" -->
<Window.Resources>
<CollectionViewSource Source="{Binding ProfilesCollection}" x:Key="profiles"/> // this corresponds to our collection in VM
</Window.Resources>
<Window.DataContext>
<vm:MainViewModel/>//Data Context of the Window
</Window.DataContext>
<Window.Background>
<VisualBrush>
<VisualBrush.Visual>
<Rectangle Width="50" Height="50" Fill="ForestGreen"></Rectangle>
</VisualBrush.Visual>
</VisualBrush>
</Window.Background>
<TabControl>
<TabControl.Resources>
<DataTemplate DataType="{x:Type model:Profile}">//this data template will be used by the TabControl
<Grid>
<TextBlock Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemsSource>
<CompositeCollection>
<TabItem Header="First Item"/>
<TabItem Header="SecondItem"/>
<CollectionContainer Collection="{Binding Source={StaticResource profiles}}"/>
</CompositeCollection>
</TabControl.ItemsSource>
</TabControl>
If you want to add more items then just use Command which would be implemented in VM and just add profile to it and enjoy the show.

WPF listbox is causing a stackoverflow when it has a button as a child

When trying to add a button as the datatemplate to a listbox, I ran into a stackoverflow. When using a textbox instead, there is no stackoverflow. What is causing this? I'm using Visual Studios 2012 Update 4.
XAML code:
<Window x:Class="StackOverflowTest.MainWindow"
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">
<ListBox ItemsSource="{Binding CausesStackOverflow}">
<ListBox.Resources>
<DataTemplate DataType="{x:Type sys:String}">
<Button Content="{Binding Path=.}"/>
</DataTemplate>
</ListBox.Resources>
</ListBox>
</Window>
C# code:
namespace StackOverflowTest
{
public partial class MainWindow
{
public string[] CausesStackOverflow { get; set; }
public MainWindow()
{
CausesStackOverflow = new string[] { "Foo" };
InitializeComponent();
DataContext = this;
}
}
}
Button is a ContentControl, which also uses a DataTemplate for its Content. A default DataTemplate ends up in recursively creating Buttons to display the "outer" Button's Content.
You should set the ListBox's ItemTemplate explicitly:
<ListBox ItemsSource="{Binding CausesStackOverflow}">
<ListBox.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

ItemControl not binding objects correctly

I have a XAML page that contains an ItemControl tag (the application uses the MVVM light framework):
<ItemsControl MinWidth="100" MinHeight="25" ItemsSource="{Binding Path=Options}" HorizontalAlignment="Left" d:LayoutOverrides="Height" Margin="10,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
This control has an item source that is a list of Option objects. The data template for this item control is as follows:
<DataTemplate DataType="{x:Type Sales:Option}">
<local:SalesOptionButton d:LayoutOverrides="Width, Height" DataContext="{Binding}"/>
</DataTemplate>
I have a view model that is associated with the SalesOptionButton control which is as follows:
public class SalesOptionButton
{
private string _name;
private Option _Option;
public ICommand SelectedOptionButtonCommand { get; set; }
public string Name
{
get { return _name; }
set { SetStructPropertyValue(ref _name, value); }
}
public Option Option
{
get { return _scriptOption; }
set { SetPropertyValue(ref _scriptOption, value); }
}
public SalesScriptOptionButton(ScriptOption option)
{
Option = option;
Name = option.OptionText;
}
protected override void RegisterForMessages()
{
SelectedOptionButtonCommand = new RelayCommand(OptionButtonSelected);
}
private void OptionButtonSelected()
{
MessengerService.Send(ScriptOptionSelectedMessage.Create(ScriptOption));
}
protected override void SetDesignTimeInfo(){}
}
Here is the XAML for the Option Control:
<UserControl [INCLUDES]>
<Button Height="25" Padding="1" MinWidth="100" Content="{Binding Name}" Command="{Binding SelectedOptionButtonCommand}"/>
</UserControl>
What this does is, for every option that is in the datasource, a button is created. These buttons should display the name of the option and, when the button is clicked, send a message to the main application that will process that click (set the chosen option).
The issue that I am seeing is that the buttons are being created but nothing else is being bound (There is no option name being displayed on the button and the button click is not working). Can anyone give me an idea as to why this isnt working like I think that it should be?
You aren't setting the data template as a property of your items control.
<ItemsControl ItemTemplate={StaticResource OptionTemplate} .../>
<DataTemplate x:Key="OptionTemplate" DataType="{x:Type Sales:Option}">
<local:SalesOptionButton d:LayoutOverrides="Width, Height" DataContext="{Binding}"/>
</DataTemplate>
It's difficult to decipher your post when bits and pieces of the code appear to be missing. You say:
This control has an item source that is a list of Option objects. The data template for this item control is as follows:
<DataTemplate DataType="{x:Type Sales:Option}">
<local:SalesOptionButton d:LayoutOverrides="Width, Height" DataContext="{Binding}"/>
</DataTemplate>
You haven't shown us your Option class - only your SalesOptionButton class. Presumably, your Option type has some property that yields the associated SalesOptionButton instance? If that's the case, then your data template is wrong here:
<local:SalesOptionButton d:LayoutOverrides="Width, Height" DataContext="{Binding}"/>
You're setting the DataContext of the SalesOptionButton to the Option instance, not to the SalesOptionButton instance. I'm guessing (and I have to) that you want something like this:
<local:SalesOptionButton d:LayoutOverrides="Width, Height" DataContext="{Binding SalesOptionButton}"/>

How do I bind a TabControl to a collection of ViewModels?

Basically I have in my MainViewModel.cs:
ObservableCollection<TabItem> MyTabs { get; private set; }
However, I need to somehow be able to not only create the tabs, but have the tabs content be loaded and linked to their appropriate viewmodels while maintaining MVVM.
Basically, how can I get a usercontrol to be loaded as the content of a tabitem AND have that usercontrol wired up to an appropriate viewmodel. The part that makes this difficult is the ViewModel is not supposed to construct the actual view items, right? Or can it?
Basically, would this be MVVM appropriate:
UserControl address = new AddressControl();
NotificationObject vm = new AddressViewModel();
address.DataContext = vm;
MyTabs[0] = new TabItem()
{
Content = address;
}
I only ask because well, i'm constructing a View (AddressControl) from within a ViewModel, which to me sounds like a MVVM no-no.
This isn't MVVM. You should not be creating UI elements in your view model.
You should be binding the ItemsSource of the Tab to your ObservableCollection, and that should hold models with information about the tabs that should be created.
Here are the VM and the model which represents a tab page:
public sealed class ViewModel
{
public ObservableCollection<TabItem> Tabs {get;set;}
public ViewModel()
{
Tabs = new ObservableCollection<TabItem>();
Tabs.Add(new TabItem { Header = "One", Content = "One's content" });
Tabs.Add(new TabItem { Header = "Two", Content = "Two's content" });
}
}
public sealed class TabItem
{
public string Header { get; set; }
public string Content { get; set; }
}
And here is how the bindings look in the window:
<Window x:Class="WpfApplication12.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<ViewModel
xmlns="clr-namespace:WpfApplication12" />
</Window.DataContext>
<TabControl
ItemsSource="{Binding Tabs}">
<TabControl.ItemTemplate>
<!-- this is the header template-->
<DataTemplate>
<TextBlock
Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<!-- this is the body of the TabItem template-->
<DataTemplate>
<TextBlock
Text="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Window>
(Note, if you want different stuff in different tabs, use DataTemplates. Either each tab's view model should be its own class, or create a custom DataTemplateSelector to pick the correct template.)
A UserControl inside the data template:
<TabControl
ItemsSource="{Binding Tabs}">
<TabControl.ItemTemplate>
<!-- this is the header template-->
<DataTemplate>
<TextBlock
Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<!-- this is the body of the TabItem template-->
<DataTemplate>
<MyUserControl xmlns="clr-namespace:WpfApplication12" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
In Prism you usually make the tab control a region so that you don't have to take control over the bound tab page collection.
<TabControl
x:Name="MainRegionHost"
Regions:RegionManager.RegionName="MainRegion"
/>
Now the views can be added via registering itself into the region MainRegion:
RegionManager.RegisterViewWithRegion( "MainRegion",
( ) => Container.Resolve<IMyViewModel>( ).View );
And here you can see a speciality of Prism. The View is instanciated by the ViewModel. In my case I resolve the ViewModel throught a Inversion of Control container (e.g. Unity or MEF). The ViewModel gets the View injected via constructor injection and sets itself as the View's data context.
The alternative is to register the view's type into the region controller:
RegionManager.RegisterViewWithRegion( "MainRegion", typeof( MyView ) );
Using this approach allows you to create the views later during runtime, e.g. by a controller:
IRegion region = this._regionManager.Regions["MainRegion"];
object mainView = region.GetView( MainViewName );
if ( mainView == null )
{
var view = _container.ResolveSessionRelatedView<MainView>( );
region.Add( view, MainViewName );
}
Because you have registered the View's type, the view is placed into the correct region.
I have a Converter to decouple the UI and ViewModel,thats the point below:
<TabControl.ContentTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding Tab,Converter={StaticResource TabItemConverter}"/>
</DataTemplate>
</TabControl.ContentTemplate>
The Tab is a enum in my TabItemViewModel and the TabItemConverter convert it to the real UI.
In the TabItemConverter,just get the value and Return a usercontrol you need.
My solution uses ViewModels directly, so I think it might be useful to someone:
First, I bind the Views to the ViewModels in the App.xaml file:
<Application.Resources>
<DataTemplate DataType="{x:Type local:ViewModel1}">
<local:View1/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ViewModel2}">
<local:View2/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ViewModel3}">
<local:View3/>
</DataTemplate>
</Application.Resources>
The MainViewModel looks like this:
public class MainViewModel : ObservableObject
{
private ObservableCollection<ViewModelBase> _viewModels = new ObservableCollection<ViewModelBase>();
public ObservableCollection<ViewModelBase> ViewModels
{
get { return _viewModels; }
set
{
_viewModels = value;
OnPropertyChanged();
}
}
private ViewModelBase _currentViewModel;
public ViewModelBase CurrentViewModel
{
get { return _currentViewModel; }
set
{
_currentViewModel = value;
OnPropertyChanged();
}
}
private ICommand _closeTabCommand;
public ICommand CloseTabCommand => _closeTabCommand ?? (_closeTabCommand = new RelayCommand(p => closeTab()));
private void closeTab()
{
ViewModels.Remove(CurrentViewModel);
CurrentViewModel = ViewModels.LastOrDefault();
}
private ICommand _openTabCommand;
public ICommand OpenTabCommand => _openTabCommand ?? (_openTabCommand = new RelayCommand(p => openTab(p)));
private void openTab(object selectedItem)
{
Type viewModelType;
switch (selectedItem)
{
case "1":
{
viewModelType = typeof(ViewModel1);
break;
}
case "2":
{
viewModelType = typeof(ViewModel2);
break;
}
default:
throw new Exception("Item " + selectedItem + " not set.");
}
displayVM(viewModelType);
}
private void displayVM(Type viewModelType)
{
if (!_viewModels.Where(vm => vm.GetType() == viewModelType).Any())
{
ViewModels.Add((ViewModelBase)Activator.CreateInstance(viewModelType));
}
CurrentViewModel = ViewModels.Single(vm => vm.GetType() == viewModelType);
}
}
}
MainWindow.XAML:
<Window.DataContext>
<local:MainWindowViewModel x:Name="vm"/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Menu Grid.Row="0">
<MenuItem Header="1" Command="{Binding OpenTabCommand}" CommandParameter="1"/>
<MenuItem Header="2" Command="{Binding OpenTabCommand}" CommandParameter="2"/>
<MenuItem Header="3" Command="{Binding OpenTabCommand}" CommandParameter="3"/>
</Menu>
<TabControl Grid.Row="1" ItemsSource="{Binding ViewModels}" SelectedItem="{Binding CurrentViewModel}">
<TabControl.ItemTemplate>
<DataTemplate DataType="{x:Type MVVMLib:ViewModelBase}">
<TextBlock Text="{Binding Title}">
<Hyperlink Command="{Binding RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}, Path=DataContext.CloseWindowCommand}">X</Hyperlink>
</TextBlock>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
</Grid>
I translated some parts to make it easier to understand, there might be some typos.

Categories