How to bind grid content to a UserControl - c#

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>

Related

WPF (Avalonia) - Draw control (button) outside TabItem?

Perhaps I'm going about this the wrong way, but my layout is in a way where I have multiple Expanders in a TabControl and I want to add an "expand all" button.
Now logically this button should be inside the tab as it would control the elements in the tab so they ought to be grouped together. Visually however this would be a waste of space as I got a lot of empty space on the Tab Header bar (not sure what the terminology is, the row with the tabheaders).
So what I'm trying to achieve is adding a button outside the content of the tab. The canvas element seems to be what I need to use and it's working as far as its repositioning the element but it gets cut off. This is much easier to explain with a picture so
(if you look hard you can see where the button is as the header covering it is slightly translucent)
Now I can position it where I'd like it to be by moving it outside the TabItem but then I would have to write code to see which tab is focussed and hide it when it's not "Current" that is focussed. That to me sounds like the wrong way to do it as the only thing I want to do is move a button which is a 'view' type of thing.
My MainWindow.axaml:
<TabControl Grid.Row="1" Grid.Column="0" VerticalAlignment="Stretch">
<TabItem Header="Current" ZIndex="1">
<ScrollViewer Classes="CrawlersInAction">
<StackPanel>
<Canvas>
<Button Canvas.Right="10" Canvas.Top="-20" ZIndex="5">Expand All</Button>
</Canvas>
<!-- My very long template code for rendering the expanders -->
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
I do have a background in HTML/CSS so I thought Zindex would the trick and tried applying it in various places without any luck.
PS: I'm using Avalonia instead of WPF but it's pretty much a cross-platform clone, so any WPF know-how probably carries over 1:1.
If you think about it, this functionality lives in the ViewModel at the same "level" as the Tab Control.
<Grid>
<TabControl Items="{Binding MyTabViewModels}" SelectedItem={Binding SelectedTab} />
</Grid>
An Instance of MyTabViewModel has a collection on it:
public ObservableCollection<MyCollectionType> Items
The item class MyCollectionType has an IsExpanded property ...
public bool IsExpanded {get;set;}
Bound to your Expander control IsExpanded property.
Shove your button into the XAML
<Grid>
<TabControl Items="{Binding MyTabViewModels}" />
<Button Commmand={Binding ExpandAllCommand} />
</Grid>
Now on your base ViewModel your ICommand can do something like:
public void ExpandAllCommandExecuted()
{
foreach(var vm in SelectedTab.Items)
{
vm.IsExpanded = true;
}
}
Good luck, this is all pseudocode but illustrates a potential pattern.
The problem seems to have originated from placing my <canvas> control inside the <scrollviewer> control. I've placed it outside it whilst trying many things it seems and it works as I wanted it to. The buttons are visible rendering on top of the tabbar (TabStrip).
My XAML is now:
<TabControl Grid.Row="1" Grid.Column="0" VerticalAlignment="Stretch">
<TabItem Header="Current">
<StackPanel>
<Canvas>
<StackPanel Orientation="Horizontal" Canvas.Right="0" Canvas.Bottom="10" Spacing="5">
<Button Command="{Binding CollapseAll}" IsEnabled="{Binding !AllAreCollapsed}">Collapse All</Button>
<Button Command="{Binding ExpandAll}" IsEnabled="{Binding !AllAreExpanded}">Expand All</Button>
</StackPanel>
</Canvas>
<ScrollViewer Classes="CrawlersInAction">
<StackPanel>
<ItemsControl Name="itemscontrol" Items="{Binding SiteInfos}" VerticalAlignment="Stretch">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Expander ExpandDirection="Down" IsExpanded="{Binding IsExpanded, Mode=TwoWay}" VerticalAlignment="Stretch">
<!-- Ommited my very long template code -->
</Expander>
<DataTemplate>
<ItemsControl.ItemTemplate>
<ItemsControl>
</StackPanel>
</ScrollViewer>
</StackPanel>
</TabItem>
</TabControl>
Codewise I ended up adding a "IsExpanded" property to my SiteInfo class that is used as the base for the expanders IsExpanded property and kept in sync by making it a two way binding as per the XAML above. The code on SiteInfo is:
public class SiteInfo : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public static readonly bool StartIsExpanded = true;
private bool _isExpanded = StartIsExpanded;
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if (value != IsExpanded)
{
_isExpanded = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsExpanded)));
}
}
}
When I create my SiteInfo objects in MainWindowViewModel I subscribe to the events (siteInfo.PropertyChanged += SiteInfo_PropertyChanged;). When the event is received and it would change if my collapse or expand all button should be disabled it sends it own PropertyChangedEvent which then enables/disabled the control.
public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
{
public new event PropertyChangedEventHandler? PropertyChanged;
public ObservableCollection<SiteInfo> SiteInfos { get; }
= new ObservableCollection<SiteInfo>();
//Change SiteInfo.StartExpanded if you want this changed.
private bool _allAreExpanded = SiteInfo.StartIsExpanded;
public bool AllAreExpanded
{
get => _allAreExpanded;
set
{
if (_allAreExpanded != value)
{
_allAreExpanded = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AllAreExpanded)));
}
}
}
//Change SiteInfo.StartExpanded if you want this changed.
private bool _allAreCollapsed = !SiteInfo.StartIsExpanded;
public bool AllAreCollapsed {
get { return _allAreCollapsed; }
set {
if (_allAreCollapsed != value)
{
_allAreCollapsed = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AllAreCollapsed)));
}
}
}
private void SiteInfo_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if(e.PropertyName == nameof(siteInfo.IsExpanded))
{
AllAreCollapsed = AreAllCollapsed();
AllAreExpanded = AreAllExpanded();
}
}
public bool AreAllCollapsed()
{
return !SiteInfos.Any<SiteInfo>( siteInfo => siteInfo.IsExpanded );
}
public bool AreAllExpanded()
{
return !SiteInfos.Any<SiteInfo>( siteInfo => siteInfo.IsCollapsed);
}
public void CollapseAll()
{
foreach(SiteInfo siteInfo in SiteInfos)
{
siteInfo.IsExpanded = false;
}
}
public void ExpandAll()
{
foreach (SiteInfo siteInfo in SiteInfos)
{
siteInfo.IsExpanded = true;
}
}
}
Figured I'd add the rest of my code in case anyone Googles this up and wants to do something similar.
So now when my program loads and everything is set to the default expanded true Expand All is disabled, Collapse all is enabled. Changing one expander to collapsed status will have both buttons enabled and collapsing all expanders will disable the Collapse All button.

Passing data between two usercontrols / Views

Using MVVM
I am trying to pass data entered in a control (a textbox in the attached code) in one view (view1) and use that data in the second view (view2). At the moment, by declaring all my views in the App.xaml file, I can bind the textblock in view2 with the information entered in the textbox in view1 and see it displayed in the said textblock. But I want to use the information entered in view2's view model as well but dont know how to access it there to use the information.
Can somebody tell me how to go about doing this? Thanks!
App.xaml [declaration of resources]
<Application.Resources>
<vws:DefaultVM x:Key="DefaultVMApp"></vws:DefaultVM>
<vws:View1 x:Key="View1App"></vws:View1>
<vws:View2 x:Key="View2App"></vws:View2>
<vm:AppVM x:Key="AppVMApp"></vm:AppVM>
<vm:View1VM x:Key="View1VMApp"></vm:View1VM>
<vm:View2VM x:Key="View2VMApp"></vm:View2VM>
</Application.Resources>
View1.xaml
<UserControl.DataContext>
<StaticResource ResourceKey="View1VMApp"></StaticResource>
</UserControl.DataContext>
<Grid Background="Aqua">
<StackPanel Margin="100">
<TextBox x:Name="firstNameTextBoxView1" Text="{Binding View1InfoClass.FirstName, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}"></TextBox>
<Button Command="{Binding Source={StaticResource AppVMApp}, Path=View2ButtonCommand}" Content="Go to view2" Height="20" Width="70" />
</StackPanel>
</Grid>
View2.xaml
<UserControl.DataContext>
<StaticResource ResourceKey="View2VMApp"></StaticResource>
</UserControl.DataContext>
<Grid Background="Beige">
<StackPanel Margin="100">
<TextBlock x:Name="View1TextBlock" Text="{Binding Source={StaticResource View1VMApp}, Path=View1InfoClass.FirstName}" ></TextBlock>
</StackPanel>
</Grid>
AppVM
public class AppVM : ObservableObject
{
//Create a property that controls current view
private static object _currentView = new DefaultVM();
public object CurrentView
{
get { return _currentView; }
private set
{
OnPropertyChanged(ref _currentView, value);
}
}
private string _textboxText;
public string TextboxText
{
get { return _textboxText; }
set
{
OnPropertyChanged(ref _textboxText, value);
}
}
public AppVM()
{
View1ButtonCommand = new RelayCommand(ShowView1, AlwaysTrueCommand);
View2ButtonCommand = new RelayCommand(ShowView2, AlwaysTrueCommand);
DefaultCommand = new RelayCommand(ShowDefault, AlwaysTrueCommand);
}
//Instantiate the relaycommands, we will need to instantiate relaycommand objects for every command we need to perform.
//This means that we will need to do this for preses of all buttons
public RelayCommand View1ButtonCommand { get; private set; }
public RelayCommand View2ButtonCommand { get; private set; }
public RelayCommand DefaultCommand { get; private set; }
public void ShowDefault(object dummy)
{
CurrentView = new DefaultVM();
}
public void ShowView1(object dummy)
{
CurrentView = new View1();
}
public void ShowView2(object dummy)
{
CurrentView = new View2();
}
public bool AlwaysTrueCommand(object dummy)
{
return true;
}
}
The fundamental problem in your code is that you have dedicated a pre-defined view model object to each of the user controls. This is really bad. A user control's data context must be left alone, for the client code (e.g. your main window) to determine, and to use for binding to specific properties that the user control exposes.
Unfortunately, there's not enough context in your question to provide a clear, complete answer. But to fix your issue, you need to do things differently:
First and foremost, "decouple" the view models you are using for your user control from the user controls themselves. Do this by adding dependency properties to each user control, and then letting the main view where the user controls are used decide what to bind to each of those dependency properties. Do not allow the user controls themselves to set their own data contexts.
Having done that, you may find that you can just use the same view model for the two user controls as for the main view. I.e. you'll set the main view's data context to the single view model, the user controls will inherit that data context, and you'll bind, for example, the TextboxText property to the appropriate declared dependency property in each user control. This way, that single property will represent state for both user controls at the same time.
One hopes that will be enough to get you back on track. If not, consider searching Stack Overflow for other questions related to view models and their relationships to user controls. For example, these questions:
Issue with DependencyProperty binding
XAML binding not working on dependency property?
WPF DataBinding with MVVM and User Controls
Other questions which don't address your scenario exactly, but which should give you some ideas for alternative ways to structure your view model(s):
MVVM : Share data between ViewModels
Sharing non control related data between WPF ViewModel and UserControl
Sharing data between different ViewModels
Sharing state between ViewModels

How can I display view inside content control in wpf

FirstView.xaml contains Something like this
<ContentControl Name="ContentControlName" Grid.Row="1" Grid.Column="0" Content="{Binding SelectionViewModel}"/>
My SelectionView.xaml contains
<TextBox x:Name="textBoxName" Text="{Binding Name}"/>
<TextBox IsReadOnly="True" Text="{Binding Uid}"/>
In the FirstViewModel I have created a property like below
private SelectionViewModel selectionViewModel;
public SelectionViewModel SelectionViewModel
{
get
{
return this.selectionViewModel;
}
}
Content control with two text box is not displayed when I run
Is the way done right?
Since you used binding, you need raise up PropertyChanged event.
Your ViewModel class (SelectionViewModel) must implement INotifyPropertyChanged.
public class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private SelectionViewModel selectionViewModel;
public SelectionViewModel SelectionViewModel
{
get
{
return this.selectionViewModel;
}
private set
{
this.selectionViewModel = value;
PropertyChanged?.Invoke(this, nameof(SelectionViewModel));
}
}
}
You passed SelectionViewModel instance to Content property of ContentControl.
Your ContentControl must have special datatemplate coupled with this view model. Otherwise, it will not work.
For example:
<ContentControl Content="{Binding SelectionViewModel}">
<ContentControl.ContentTemplate>
<DataTemplate DataType="{x:Type local:SelectionViewModel}">
<!-- Here is your template -->
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
Also you shouldn't use same names for type SelectionViewModel and property SelectionViewModel.
Since you wasn't provide a source code we can't figure out the exact cause of your error.
I hope it was helpful for you.

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.

Adding to XAML WPF from ViewModel (MVVM)

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.

Categories

Resources