Adding to XAML WPF from ViewModel (MVVM) - c#

first attempt at MVVM and WPF (steep learning curve).
In my ViewModel I want to run the following code to add a "layoutDocument" which is an AvalonDock layout into my Mainform UI.
ViewModel class:
LayoutDocument layoutDocument = new LayoutDocument { Title = "Plan Layout" };
Window mainWindow = Application.Current.Windows.OfType<Window>().Where(x => x.Name == "MainWindow").FirstOrDefault();
if (mainWindow != null)
{
mainWindow.mainPanel.Children.Add(layoutDocument);
}
The above code gives me the following error:
"'Window' does not contain definition for 'mainPanel' and no extension method for 'mainPanel'".
Note in my XAML below that "LayoutDocumentPane" does contain a name "mainPanel".
I have tried adding the above code directly into my MainForm View Class (excluding the Application.Current.Windows.OfType and If statement bit) and just including the:
mainPanel.Children.Add(layoutDocument);
And it works fine (a new layout is created in my MainForm when I click the button).
However, as I want to stickto MVVM this is not a suitable solution.
How can I add "layoutDocument" to MainWindow from ViewModel? Thanks in advance.
An extract of my XAML looks like this:
<Window x:Class="LiveExplorer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:LiveExplorer"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
xmlns:xcad="http://schemas.xceed.com/wpf/xaml/avalondock"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:vm="clr-namespace:WpfApp1.ViewModel">
<Grid> etc etc etc here---
<xcad:LayoutDocumentPaneGroup>
<xcad:LayoutDocumentPane x:Name="mainPanel">
<xcad:LayoutDocument ContentId="document1" Title="Document 1" >
<Button Content="Document 1 Content" HorizontalAlignment="Center" VerticalAlignment="Center"
Command="{Binding NewPlanCommand, Source={StaticResource viewModel}}"
/>
</xcad:LayoutDocument>
<xcad:LayoutDocument ContentId="document2" Title="Document 2">
<TextBox Text="Document 2 Content" AcceptsReturn="True"/>
</xcad:LayoutDocument>
</xcad:LayoutDocumentPane>
</xcad:LayoutDocumentPaneGroup >
EDIT:
Whilst the accepted answer does not answer the question in terms of MMVM, it does correct the coding error.

What you've tried to implement does not follow the MVVM pattern. You need to take care of 3 things to get started:
ViewModels
Initialize the ViewModel binded to the window
Binding ViewModel to the UI in XAML
ViewModels:
Create a viewmodel that will be binded to your MainWindow and create an observable collection inside that MainWindowViewModel that contains a list of object that will contain data that can be used in the UI:
public ObservableCollection<LayoutDocumentViewModel> LayoutDocument {get;set;}
Make sure that both the MainWindowViewModel and the LayoutDocumentViewModel inherits from INotifyPropertyChanged(Implement Property Change Notification) or if you use MVVMLight (or similar) from ViewModelBase.
The LayoutDocumentViewModel is just a ViewModel that will be used to store information about your layout document and that can be binded to the UI.
public LayoutDocumentViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName]
string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
// Call OnPropertyChanged whenever the property is updated
OnPropertyChanged("Name");
}
}
}
I would strongly recommend that you use MVVMLight (or similar) or put the INotifyPropertyChange code into a base class i.e. ViewModelBase for example.
For simplicity sake in this example, I'm initializing the observable collection and creating a couple of document layouts objects directly in the MainWindowViewModel but you'll need to research this further and find out where it is appropriate for you to initialize and/or create these.
public MainPageViewModel()
{
DocumentLayouts = new ObservableCollection();
DocumentLayouts.Add(new DocumentLayout {Name="Layout1"});
DocumentLayouts.Add(new DocumentLayout {Name="Layout2"});
}
The above takes care of creating your MainWindowViewModel and layout documents.
Initializing MainViewModel (and binded to the MainWindow.xaml). Note this is a quick and dirty way to get you started and you should really look into IoC containers.
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
Finally, bind your ViewModel & UI
XAML:
<Grid>
<ItemsControl ItemsSource="{Binding LayoutDocuments}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Label Content="{Binding Name}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
Note: Just replace the Label by your LayoutDocument control and bind it to the relevant element properties you have declared in LayoutDocumentViewModel.
Hope this helps get you started.

This is not related to MVVM but to be able access the mainPanel you need to cast the returned Window to a MainWindow:
MainWindow mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
if (mainWindow != null)
{
mainWindow.mainPanel.Children.Add(layoutDocument);
}
A view model shouldn't access any window directly though. This breaks the MVVM pattern.

Related

Learning MVVM. Calling Methods

I'm currently learning how MVVM works and gettings a bit confused.
What I Have Now: I've got a MainWindow.xaml and have made a button that adds in UserControl1.xaml adding it to a ContentControl, which all works great. I've got a folder named ViewModels with a class named SettingsViewModel.cs and another folder named Views with a UserControl named SettingsView.xaml
What I'm trying to figure out: In the User Control I'll have things like buttons, checkboxes, and some other stuff, I want to be able to have a button press in the MainWindow to call a method where I can do stuff like changing the visibility of items among other things. How I go about calling this method from the MainWindow and where to put the method [SettingsViewModels.cs or SettingsView.xaml].
I'm still very new to programming so I'm probability leaving out a bunch of info, so ask me any question.
I have accually got this to work the other way around; calling a method in MainWindow from a UserControl like this...
//this is in the UserControl
private void Button1_Click(object sender, RoutedEventArgs e)
{
MainWindow callMethod = (MainWindow)Application.Current.MainWindow;
callMethod.MyMethod1();
}
//this is in the MainWindow
pubic void MyMethod1()
{
//whatevery i want here
}
There are a couple of things to consider. In MVVM, View communicate to ViewModel through bindings and ViewModel communicate to the View through events typical from INotifyPropertyChanged and ICollectionChanged. Buttons should be binded to a property of type ICommand. The ViewModel should not know about WPF control stuff like Visibility etc.
To change visibility you use an IValueConverter called BooleanToVisiblityConverter.
Without quite understanding what you are asking, here is some pseudo code of how I would do it.
The structure of your files doesn't matter, but dividing them into Views and ViewModels is a good idea.
Disclaimer: This code will not run, shows only the concept. I left Visual Studio on my other computer.
ViewModel:
public class MainWindowViewModel
{
public ICommand OpenCommand { get; }
public object Child { get; private set; }
public MainWindowViewModel()
{
OpenCommand = new RelayCommand(Open);
}
private void DoOpen()
{
Child = new ChildViewModel();
}
}
public class ChildViewModel
{
public bool ShowSomething { get; }
}
public class Program
{
private void SomeStartupLogic()
{
var window = new MainWindow();
windows.DataContext = new MainWindowViewModel(); // or use an IoC container
window.Show();
}
}
View
<Window class="MainWindow">
<Window.Resources>
<DataTemplate DataType="{x:Type ChildViewModel}">
<ChildView/>
</DataTemplate>
</Window.Resources>
<Grid>
<ContentControl Content="{Binding Child}"/>
<Button Command="{Binding OpenCommand}"/>
</Grid>
</Window>
<UserControl class="ChildView">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConvert"/>
</UserControl.Resources>
<Grid>
<TextBlock Text="Something" Visibility="{Binding ShowSomething, Converter={StaticResource BooleanToVisibilityConvert}/>
</Grid>
</UserControl>
Links
MVVM
Commands
PropertyChanged

How to bind grid content to a UserControl

I have a Grid and want to show user control as child of the grid or content of the grid. When a button is clicked, a few user controls will be shown dependent on the cases. Please check the xaml part and code behind.
<Grid x:Name="ContentPanel" Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Margin="5,5,5,5">
</Grid>
I want to bind Grid content to the below activeUserControl object.
public class MainVM
{
public UserControl activeUserControl;
Stability stability;
Tank tank;
public MainVM()
{
stability = new Stability();
tank = new Tank();
activeUserControl = stability;
stability.Visibility = Visibility.Visible;
}
}
The problem is that you cannot directly bind to the Children collection of a Grid, because it is not a DependencyProperty. You would have to implement attached properties or a behavior to do so. However, you can put a ContentControl into your Grid or replace it as a workaround. Then bind its Content to the activeUserControl property in your view model. Usually properties start with a capital letter, so I adapted it.
<Grid x:Name="ContentPanel" Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5,5,5,5">
<ContentControl Content="{Binding ActiveUserControl}"/>
</Grid>
Make sure that your MainVM is set as DataContext in any of the parent controls, otherwise this binding will not work. activeUserControl must be a property to make it bindable. Implement INotifyPropertyChanged in your MainVM, so that the ContentControl gets notified when the property changes and adapts its Content.
// Implement "INotifyPropertyChanged" so controls get notified about property changes
public class MainVM : INotifyPropertyChanged
{
// Backing field of the "ActiveUserControl" property
private UserControl _activeUserControl;
public UserControl ActiveUserControl
{
get => _activeUserControl;
set
{
// Only set the value if it has changed
if (_activeUserControl != value)
{
_activeUserControl = value;
// Signal to the control that it needs to update the value
OnPropertyChanged(nameof(ActiveUserControl));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
public MainVM()
{
// ...your code.
ActiveUserControl = stability;
// ...your code.
}
protected virtual void OnPropertyChanged(string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
This should solve your problem, but your code is still a mix of MVVM and code-behind. Ideally, you should not have any UI control references in a view model.
Consider creating Tank and Stability view models using INotifyPropertyChanged and data templates to display them, instead of UserControls in your view model. You can still use the ContentControl in this case.
<ContentControl Content="{Binding activeUserControl}">
<ContentControl.Resources>
<DataTemplate DataType={x:Type TankViewModel}>
<!-- ...data template equivalent of your "Tank" user control. -->
</DataTemplate>
<DataTemplate DataType={x:Type StabilityViewModel}>
<!-- ...data template equivalent of your "Stability" user control. -->
</DataTemplate>
</ContentControl.Resources>
</ContentControl>

Set the DataContext of a View on a Navigation in XAML/WPF using MVVM

in my WPF-application i have multiple Views in a main window and i tried to implement a navigation between those.
My Problem is that i can't set the DataContext attribute of the views.
My MainWindowViewModel:
public Class MainWindowViewModel
{
public MainScreenViewModel mainScreenViewModel { get; set; }
public LevelViewModel levelViewModel { get; set; }
public ViewModelBase CurrentViewModel
{
get { return _currentViewModel; }
set
{
_currentViewModel = value;
RaisePropertyChanged(nameof(CurrentViewModel));
}
}
private AdvancedViewModelBase _currentViewModel;
}
My MainWindow:
<Window.Resources>
<DataTemplate DataType="{x:Type ViewModels:MainScreenViewModel}">
<views:MainScreen />
</DataTemplate>
<DataTemplate DataType="{x:Type ViewModels:LevelViewModel}">
<views:LevelView />
</DataTemplate>
</Window.Resources>
<Border>
<StackPanel>
<UserControl Content="{Binding Path=CurrentViewModel, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></UserControl>
</StackPanel>
</Border>
So the main idea is that the CurentViewModel shows on which View the navigation is at the moment (the DataTemplate shows the coreponding View to the ViewModel).
The Problem is that the shown View doesn't get the DataContext (so the properties mainScreenViewModel/levelViewModel of the MainWindowViewModel), it creates a new instance of the ViewModels.
Is it possible to hand over the properties as a DataContext to the View from the DataTemplate?
Thanks for your help!
The Content property contains
An object that contains the control's content
This means it is not the correct property to bind the view model. Instead you need to bind it to the DataContext property which contains
The object to use as data context
Now the defined templates are selected by their type like defined in the resources.
This means your code is almost correct, just change the binding of the CurrentViewModel like
<UserControl DataContext="{Binding CurrentViewModel}"/>
to get your code to work.

WPF Mvvm navigation with parameters

Following this tutorial (among others) and reading questions asked here I've constructed a navigation mechanism that will allow me to pass parameters between my ViewModels:
Object base - every view model inherits from it:
public abstract class ObjectBase : INotifyPropertyChanged
{
//INotifyPropertyChanged members
...
//Navigation handling
public abstract ObjectBase BackLocation { get; }
public abstract event Action<ObjectBase> NavigateTo;
public abstract string ViewHeader { get; }
}
MainViewModel - in charge of navigation:
public class MainViewModel : ObjectBase
{
private ObjectBase _selectedView;
private CommandBase _backCommand;
public MainViewModel()
{
SelectedView = new FirstViewModel();
}
public ObjectBase SelectedView
{
get { return _selectedView; }
set
{
SetProperty(ref _selectedView, value);
//register to the navigation event of the new view
SelectedView.NavigateTo += (target)=> { SelectedView = target; };
}
}
//This command is bound to a Back button on the main view
public CommandBase BackCommand
{
get { return _backCommand ?? (_backCommand = new CommandBase(Back)); }
}
private void Back(object obj)
{
if (SelectedView.BackLocation != null)
{
SelectedView = SelectedView.BackLocation;
}
else
{
Application.Current.Shutdown();
}
}
}
And the main view:
<Window ...
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType="{x:Type vm:FirstViewModel}">
<views:FirstView/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:SecondViewModel}">
<views:SecondView/>
</DataTemplate>
</Window.Resources>
<ContentPresenter Content="{Binding SelectedView}"/>
</Window>
My problem is: If I set the DataTemplates in the main view like the above it makes each view aware of it's DataContext so if I want to add the DataContext explicitly to a view in order to use intellisense like this:
<UserControl x:Class="Wpf_NavigationTest.Views.FirstView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="clr-namespace:Wpf_NavigationTest.ViewModels">
<!--this causes the view model's constructor to get called again-->
<UserControl.DataContext>
<viewModels:FirstViewModel/>
</UserControl.DataContext>
<Grid>
<TextBlock Text="User control 1" FontSize="40"/>
</Grid>
the View Model's constructor is called twice, losing the parameters passed by the Navigate event.
The problem here is that you are setting the DataContext inside your UserControl, and also in your main view model.
<UserControl.DataContext>
<viewModels:FirstViewModel/>
</UserControl.DataContext>
The code above is instantiating a new FirstViewModel every time this UserControl is created. Therefore when the control gets created by the ContentControl (based on the DataTemplate), it then goes ahead and also creates a new FirstViewModel.
So, the solution here is to remove the UserControl.DataContext declaration in the UserControl, and you can instead set the DataContext of the ContentControl to that of your SelectedView.
<ContentPresenter Content="{Binding SelectedView}"
DataContext="{Binding SelectedView}"/>
In order to use multiple view models to a single view, you can simply add another DataTemplate:
<DataTemplate DataType="{x:Type vm:ThirdViewModel}">
<views:SecondView/>
</DataTemplate>
For Design-Time data (to get the intellisense), you can make use of d:DataContext as explained in this article.
This will require you to set up some view models as static resources, I would recommend creating them in a separate ResourceDictionary.

Binding to properties in both the ViewModel and CodeBehind

I have what I'm sure is a ridiculously ignorant question, but I'm asking it anyways because I've searched and searched and either don't understand the solutions I'm seeing or not finding exactly the answer I seek.
I have an MVVM application. My XAML is setup with the DataContext set to the VM where the data items on the screen are populated from the VM's properties. My CodeBehind doesn't fiddle with the data, only things relating to the screen.
What I want to do now is bind certain UI elements to properties in the foo.xaml.cs (CodeBehind) file. For example, I want to specify FontSize's bound to properties in the CB so that in the WindowInitialized handler in the CB, it can detect screen sizes and change one variable to which all the screen items' FontSize= are bound.
I can solve this the wrong way by creating a public property in my VM and then "inject" the value from the CB into the VM. I know that will work, but it's a roundabout way to get the behavior I want, it's not at all straightforward, and I feel confident it's the wrong way to proceed.
I searched around and have tried things like:
FontSize="{Binding RelativeSource={RelativeSource Self},Path="MyFontSize"
(where "MyFontSize" is a public int property) and a variety of other examples I found, but none have worked.
So specifically, if my CodeBehind class is called NameChangeSetupMainWindow and that's where the "MyFontSize" property lives,
public partial class NameChangeSetupMainWindow : Window
{
private int m_fontSize = 14;
public int MyFontSize
{
get { return m_fontSize; }
set
{
if (m_fontSize != value))
{
m_fontSize = (value > 0) ? value : 10;
}
}
}
...
... rest of the class...
...
}
and the VM is called NameChangeSetupViewModel and that's where the "real" data lives and the DataContext points ala:
<Window.DataContext>
<local:NameChangeSetupViewModel/>
</Window.DataContext>
what is the syntax in XAML to bind just those UI items (tooltips related to the UI, font sizes, etc) to variables in the CodeBehind instead of housing them in the VM?
Thanks in advance for any guidance you can supply.
You can use RelativeSource AncestorType to bind to properties of the view itself:
<TextBlock FontSize="{Binding RelativeSource={RelativeSource AncestorType=Window},Path=MyFontSize}" />
Using ElementName should work as well:
<Window x:Name="window">
<TextBlock FontSize="{Binding ElementName=window,Path=MyFontSize}" />
</Window>
Edit
Here is an example that I've confirmed working:
XAML
<Window x:Class="WpfAbc.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"
ToolTip="{Binding RelativeSource={RelativeSource Self},Path=MyToolTip}"
>
<Grid>
<TextBlock Text="hello world" FontSize="{Binding RelativeSource={RelativeSource AncestorType=Window},Path=MyFontSize}" />
</Grid>
</Window>
Code Behind
public partial class MainWindow : Window
{
private int m_fontSize = 20;
public int MyFontSize
{
get { return m_fontSize; }
set
{
if (m_fontSize != value)
{
m_fontSize = (value > 0) ? value : 10;
}
}
}
public string MyToolTip
{
get { return "hello world"; }
}
public MainWindow()
{
InitializeComponent();
}
}
Articles on this topic:
The RelativeSource markup extension
XAML binding declarations
Related background:
"Namescopes" in XAML (when binding to a source using "ElementName", the source element must be in the same namescope)
Visual tree vs logical tree in XAML (elements not in the visual tree, like Popup and ContextMenu, do not inherit DataContext. Binding from these elements requires a workaround like the "data context spy" technique.)

Categories

Resources