Why does Treeview if MVVM model inherit expanded across all tabs? - c#

I do not understand why when I toggle the expanding of a tab's treeview in WPF, that it then affects the expanding of all tab's treeviews. I want each tab's treeview to be independent from one another. It's a very simple MVVM setup with a few classes.
Here are the files from the project
MainWindow.xaml
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:data="clr-namespace:WpfApplication1"
xmlns:view="clr-namespace:WpfApplication1.View"
WindowStartupLocation="CenterScreen"
Title="MainWindow" Height="350" Width="250">
<Window.DataContext>
<data:ViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!--<Button Content="Add" Command="{Binding AddCommand}" Grid.Row="0"></Button>-->
<TabControl x:Name="tabControl1" ItemsSource="{Binding TabItems}" Grid.Row="1" Background="LightBlue">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}" VerticalAlignment="Center"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<view:TabItemView />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
TabItemView.xaml
<UserControl x:Class="WpfApplication1.View.TabItemView"
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"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Content}" />
<TreeView Grid.Row="1" Background="Transparent">
<TreeViewItem Header="Favorites">
<TreeViewItem Header="USA"></TreeViewItem>
<TreeViewItem Header="Canada"></TreeViewItem>
<TreeViewItem Header="Mexico"></TreeViewItem>
</TreeViewItem>
</TreeView>
</Grid>
</UserControl>
ViewModel.cs
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Input;
namespace WpfApplication1
{
public class ViewModel
{
private ObservableCollection<TabItem> tabItems;
public ObservableCollection<TabItem> TabItems
{
get { return tabItems ?? (tabItems = new ObservableCollection<TabItem>()); }
}
public ViewModel()
{
TabItems.Add(new TabItem { Header = DateTime.Now.ToString("Tab 1"), Content = DateTime.Now.ToString("F") });
TabItems.Add(new TabItem { Header = DateTime.Now.ToString("Tab 2"), Content = DateTime.Now.ToString("F") });
TabItems.Add(new TabItem { Header = DateTime.Now.ToString("Tab 3"), Content = DateTime.Now.ToString("F") });
}
}
public class TabItem
{
public string Header { get; set; }
public string Content { get; set; }
}
}

The TabControl reuses the same content element for each item. All it does is change the bound data context (i.e. your view model), updating the elements within the templated element that are bound to the view model. So the other state of the view remains the same as you switch from one tab to the next.
It is possible to force a new content element to be created each time you change the tab; one way of doing this is to declare the content element's template as a resource, add x:Shared="False" to the resource declaration, and then use the resource as the value for a Setter applied in a Style targeting the TabItem type.
Going through the setter to apply the template to each TabItem is required — and by TabItem here I mean the WPF TabItem, not your view model class, the name of which I'd change, to avoid confusion, if I were you. Using x:Shared won't help if you just set the TabControl.ContentTemplate directly once.
For example:
<TabControl.Resources>
<DataTemplate x:Key="tabItemTemplate" x:Shared="False">
<l:TabItemView />
</DataTemplate>
<s:Style TargetType="TabItem">
<Setter Property="ContentTemplate" Value="{StaticResource ResourceKey=tabItemTemplate}"/>
</s:Style>
</TabControl.Resources>
However, this has the opposite effect: rather than keeping the state for each item as you switch from tab to tab, the view state is reset entirely, because a whole new content element is created every time you switch.
If this is acceptable to you, then that will work fine. If however you are looking for each tab to retain whatever configuration it was in when the user last viewed that tab, you will have to preserve that state yourself. For example, you could add a bool property to your view model to remember the current setting for each item and bind that to the IsExpanded property for the top-level TreeViewItem:
View model:
public class TabItem
{
public string Header { get; set; }
public string Content { get; set; }
public bool IsExpanded { get; set; }
}
View:
<UserControl x:Class="TestSO33125188TreeViewTemplate.TabItemView"
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"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Content}" />
<TreeView Grid.Row="1" Background="Transparent">
<TreeViewItem Header="Favorites" IsExpanded="{Binding IsExpanded, Mode=TwoWay}">
<TreeViewItem Header="USA"></TreeViewItem>
<TreeViewItem Header="Canada"></TreeViewItem>
<TreeViewItem Header="Mexico"></TreeViewItem>
</TreeViewItem>
</TreeView>
</Grid>
</UserControl>
Note that for this to work, you need to explicitly set the binding Mode property to TwoWay, as the default is OneWay and won't otherwise copy the current TreeViewItem.IsExpanded value back to the view model.
Note also that for your specific example, you can get away with a simple container view model, without implementing INotifyPropertyChanged or similar mechanism. WPF is forced to copy the updated property value back to the view whenever the current tab is changed. But personally, I would go ahead and add the INotifyPropertyChanged implementation; it would be too easy to find yourself reusing the technique in a different scenario where WPF doesn't automatically detect that you've updated the property value, causing a frustrating bug that takes a while to track down and fix.

Related

WPF TabControl - Add New Tab With Content Template

I am trying to make a demo application to help me understand WPF/MVVM. I have been struggling for 3 days looking at various tutorials and threads. I want to make a tab control with a new tab button (like here) that lets the user create a new tab with specified content template. I create my user control that I want to be the template here:
<UserControl x:Class="MvvmTest.UserControl1"
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"
xmlns:local="clr-namespace:MvvmTest"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<ListView d:ItemsSource="{d:SampleData ItemCount=5}">
<ListView.View>
<GridView>
<GridViewColumn/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</UserControl>
It is just a control with a ListView. So, I want this ListView to be in any new tab that is opened.
Here is my main window with the actual tab control:
<Window x:Class="MvvmTest.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:local="clr-namespace:MvvmTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Button Content="New Tab" Margin="703,6,10,401" Click="Button_Click"/>
<TabControl Name= "TabControl1" Margin="0,33,0,-33" Grid.ColumnSpan="2">
</TabControl>
</Grid>
</Window>
In this code-behind, I try to create a new tab programmatically and set the content template to the new control.
private void Button_Click(object sender, RoutedEventArgs e)
{
TabControl1.Items.Add(new TabItem() { ContentTemplate = UserControl1 });
}
This fails. I also tried setting properties in the XAML which also failed. I'm not sure what else to try.
If you're trying to use MVVM, where is your view model? The approach you have so far is not very MVVM because you're using code-behind to add tab items. The MVVM approach would be to bind the ItemSource property of the TabControl to a collection of items and let the view model add the items for you. You also cannot use a UserControl as a ContentTemplate like that without wrapping it in a DataTemplate definition.
The first thing to do is to define some view models:
// MvvmLight (from NuGet) is included for it's INotifyPropertyChanged
// (ViewModelBase) and ICommand (RelayCommand) classes. INotifyPropertyChanged
// is how Binding works between the View and the View Model. You could
// implement these interfaces yourself if you wanted to.
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Collections.ObjectModel;
using System.Windows.Input;
namespace MvvmTest
{
public class MainWindowViewModel : ViewModelBase
{
// store our list of tabs in an ObservableCollection
// so that the UI is notified when tabs are added/removed
public ObservableCollection<TabItemViewModel> Tabs { get; }
= new ObservableCollection<TabItemViewModel>();
// this code gets executed when the button is clicked
public ICommand NewTabCommand
=> new RelayCommand(() => Tabs.Add(new TabItemViewModel()
{ Header = $"Tab {Tabs.Count + 1}"}));
}
public class TabItemViewModel : ViewModelBase
{
// this is the title of the tab, note that the Set() method
// invokes PropertyChanged so the view knows if the
// header changes
public string Header
{
get => _header;
set => Set(ref _header, value);
}
private string _header;
// these are the items that will be shown in the list view
public ObservableCollection<string> Items { get; }
= new ObservableCollection<string>() { "One", "Two", "Three" };
}
}
Then you can fix your XAML so that it refers to the view-models that you defined. This requires defining the DataContext for your MainWindow and binding the elements of MainWindow to properties on the view model:
<Window x:Class="MvvmTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MvvmTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<!--Set the DataContent to be an instance of our view-model class -->
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!--The Command of the button is bound to the View Model -->
<Button Grid.Row="0" HorizontalAlignment="Right" Width="100"
Content="New Tab"
Command="{Binding NewTabCommand}" />
<!--ItemsSource is bound to the 'Tabs' property on the view-
model, while DisplayMemeberPath tells TabControl
which property on each tab has the tab's name -->
<TabControl Grid.Row="1"
ItemsSource="{Binding Tabs}"
DisplayMemberPath="Header">
<!--Defining the ContentTemplate in XAML when is best.
This template defines how each 'thing' in the Tabs
collection will be presented. -->
<TabControl.ContentTemplate>
<DataTemplate>
<!--The UserControl/Grid were pointless, so I
removed them. ItemsSource of the ListView is
bound to an Items property on each object in
the Tabs collection-->
<ListView ItemsSource="{Binding Items}">
<ListView.View>
<GridView>
<GridViewColumn Header="Some column"/>
</GridView>
</ListView.View>
</ListView>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
The result is that when you press the button, a new tab gets created and shown

UWP - x:Bind and Binding - FlipView inside AdaptativeGridView - Use only x:Bind

new to .NET ecosystem.
The following code is working fine following MVVM in a blank UWP app.
<Page
x:Class="UWP.Blank.Mvvm.App01.Views.PrincipalPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UWP.Blank.Mvvm.App01"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
xmlns:md="using:UWP.Blank.Mvvm.App01.Models"
xmlns:vm="using:UWP.Blank.Mvvm.App01.ViewModels"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
x:Name="ThisPrincipalPage">
<Page.Resources>
<DataTemplate x:Name="FlipView_ItemTemplate" x:DataType="md:MyFlipViewItem">
<Grid>
<Image Source="{x:Bind ImageLocation}" Stretch="Uniform" HorizontalAlignment="Center"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="AdaptativeGridView_ItemTemplate">
<Grid
Background="White"
BorderBrush="Black"
BorderThickness="1">
<FlipView Width="300" Height="300"
ItemTemplate="{StaticResource FlipView_ItemTemplate}"
ItemsSource="{Binding ElementName=ThisPrincipalPage, Path=ViewModel.FlipViewData}"/>
</Grid>
</DataTemplate>
</Page.Resources>
<Grid>
<controls:AdaptiveGridView x:Name="AdapGridView1"
ItemsSource="{x:Bind Path=ViewModel.AdaptativeGridViewData}"
ItemTemplate="{StaticResource AdaptativeGridView_ItemTemplate}"/>
</Grid>
</Page>
Which looks like:
enter image description here
The code behind is:
public sealed partial class PrincipalPage : Page
{
// Reference to our view model.
public PrincipalViewModel ViewModel { get; set; }
public PrincipalPage()
{
this.InitializeComponent();
this.ViewModel = new PrincipalViewModel();
}
}
And the ViewModel is an instance of this class:
public class PrincipalViewModel
{
public ObservableCollection<int> AdaptativeGridViewData { get; set; } = new ObservableCollection<int>() { 1, 2, 3, 4 };
public ObservableCollection<MyFlipViewItem> FlipViewData { get; set; } = new ObservableCollection<MyFlipViewItem>();
...
}
that provides two lists, one for the AdaptativeGridView control, and another for all the FlipViews inside of it.
My question is, is there any way I can use x:Bind only? Basically, because it claims to be faster. In case it is not possible, x:Bind is not a substitute of Binding, and makes everything even more confusing.
I tried two basic "intuitive options" which don't work.
Option 1, where ItemsSource property of the FlipView is changed to use x:Bind. Error claims:
Invalid binding path 'ViewModel.FlipViewData' : Property 'ViewModel' not found on type 'DataTemplate'.
Seems FlipView's DataTemplate has a DataContext different than expected. Maybe the DataContext is pointing to ViewModel.AdaptativeGridViewData, but, who knows? It is XAML. This is one of the reasons I am finding XAML an inconvenient since it behaves like a "black box". You know how to do something or you are in trouble.
In this case I tried to use classic Binding with "FindAncestor" utility to point back to ViewModel. In UWP however, AncestorType is not implemented.
(i.e., alike --> ItemsSource="{Binding FlipViewData, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type AdaptativeGridView}}}" )
<DataTemplate x:Key="AdaptativeGridView_ItemTemplate">
<Grid
Background="White"
BorderBrush="Black"
BorderThickness="1">
<FlipView Width="300" Height="300"
ItemTemplate="{StaticResource FlipView_ItemTemplate}"
ItemsSource="{x:Bind Path=ViewModel.FlipViewData}"/>
</Grid>
</DataTemplate>
I am curious since I am not explicitely setting the x:DataType in the DataTemplate. So, why the DataContext is not the same as the AdaptativeGridView or any "normal" control in the Page?
Option 2, which explicitely sets x:DataType of the DataTemplate (note this is indeed working in the other DataTemplate "FlipView_ItemTemplate"). However, for the "AdaptativeGridView_ItemTemplate", there is a runtime error.
Exception thrown: 'System.InvalidCastException' in UWP.Blank.Mvvm.App01.exe
and no more clues.
<DataTemplate x:Key="AdaptativeGridView_ItemTemplate" x:DataType="vm:PrincipalViewModel">
<Grid
Background="White"
BorderBrush="Black"
BorderThickness="1">
<FlipView Width="300" Height="300"
ItemTemplate="{StaticResource FlipView_ItemTemplate}"
ItemsSource="{x:Bind FlipViewData}"/>
</Grid>
</DataTemplate>
Any experience or suggestions are welcome.

Resizing of ToolWindow and contents in WPF

We have a VS extension. The user uses a ToolWindowPane to interact with the extension. The height and width of the ToolWindowPane depends on how the user has their VS environment set up, and currently the contents of the ToolWindowPane does not resize properly.
So here is the window:
public class SymCalculationUtilitiesWindow : ToolWindowPane
{
/// <summary>
/// Initializes a new instance of the <see cref="SymCalculationUtilitiesWindow"/> class.
/// </summary>
public SymCalculationUtilitiesWindow() : base(null)
{
this.Caption = "Sym Calculation Utilities";
this.ToolBar = new CommandID(new Guid(Guids.guidConnectCommandPackageCmdSet), Guids.SymToolbar);
// This is the user control hosted by the tool window; Note that, even if this class implements IDisposable,
// we are not calling Dispose on this object. This is because ToolWindowPane calls Dispose on
// the object returned by the Content property.
this.Content = new UtilitiesView();
}
}
So the UtilitiesView is the default view. Here is the xaml:
<UserControl
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"
xmlns:local="clr-namespace:Sym.VisualStudioExtension" x:Class="Sym.VisualStudioExtension.Engines.UtilitiesView"
xmlns:engines="clr-namespace:Sym.VisualStudioExtension.Engines"
Background="{DynamicResource VsBrush.Window}"
Foreground="{DynamicResource VsBrush.WindowText}"
mc:Ignorable="d"
local:ViewModelLocator.AutoWireViewModel="True"
x:Name="MyToolWindow" Height="800" Width="400">
<UserControl.Resources>
<DataTemplate DataType="{x:Type engines:CalcEngineViewModel}">
<engines:CalcEngineView/>
</DataTemplate>
<DataTemplate DataType="{x:Type engines:TAEngineViewModel}">
<engines:TAEngineView/>
</DataTemplate>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="207*" />
<RowDefinition Height="593*"/>
</Grid.RowDefinitions>
<Grid x:Name="MainContent"
Grid.Row="1" Grid.RowSpan="2">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<ContentControl Content="{Binding CurrentEngineViewModel}" Grid.RowSpan="2"/>
</Grid>
</Grid>
and the user makes a choice which determines the CurrentEngineViewModel.
The following is the xaml of one of the possible CurrentEngineViewModels:
<UserControl x:Class="Sym.VisualStudioExtension.Engines.CalcEngineView"
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"
xmlns:domain="clr-namespace:Sym.Engines.Calculation.Builder.Domain;assembly=Sym.Engines.Calculation"
xmlns:core="clr-namespace:Sym.Core.Domain;assembly=Sym.Core"
xmlns:helper="clr-namespace:Sym.VisualStudioExtension.Helper_Classes"
xmlns:local="clr-namespace:Sym.VisualStudioExtension"
local:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d"
d:DesignHeight="800" d:DesignWidth="400">
<UserControl.Resources>
<ContextMenu>
...
</ContextMenu>
</UserControl.Resources>
<Grid Margin="-20,-20,-20,-20">
<Grid.RowDefinitions>
<RowDefinition Height="8*"/>
</Grid.RowDefinitions>
<TabControl x:Name="tabControl" HorizontalAlignment="Left" Height="617" Margin="20,54,0,0" VerticalAlignment="Top" Width="357">
<TabItem Header="Calculation Files">
<ListBox Name="CalcFilesListBox"
Margin="20" ItemsSource="{Binding CalcFilesList}"
ContextMenu="{StaticResource NewEditDeleteContextMenu}"
Tag="{x:Type core:CalcFile}">
...
</ListBox>
</TabItem>
<TabItem Header="Template Files" Height="22" VerticalAlignment="Top">
<TreeView ItemsSource="{Binding TemplateFamilyList}"
Margin="20"
ContextMenu="{StaticResource NewEditDeleteContextMenu}"
Tag="{x:Type domain:TemplateParameter}">
<TreeView.Resources>
...
</TreeView.Resources>
</TreeView>
</TabItem>
<TabItem Header="Advanced Calc Files">
<ListBox Margin="20"
ItemsSource="{Binding AdvancedCalcFilesList}"
ContextMenu="{StaticResource NewEditDeleteContextMenu}"
Tag="{x:Type domain:TemplateCalculation}">
...
</ListBox>
</TabItem>
</TabControl>
<Label x:Name="label" Content="{Binding Path=Title}" HorizontalAlignment="Left" Height="27" Margin="10,22,0,0" VerticalAlignment="Top" Width="367"/>
</Grid>
It seems to me that on the ToolWindowPane, there is a Grid, and on the Grid another Grid, and on that Grid, a Tabcontrol. From this post it seems that some controls do not resize. So would that mean that the TabControl can't resize even if it is on a Grid?
When the user resizes the ToolWindowPane, the contents does not resize.
How can I achieve correct resizing in this situation?
The base of the problem is the explicit Width and Height values are being set and preventing the controls from fitting into their container.
To make the page salable across resolutions/devices, you must ensure that there are no fixed width/height property specified; instead we can use them in percentage (%) which will make the controls scale based on available viewport. Try replacing all the fixed height & width properties for your control with percentages.
if for some reasons the controls are getting squeezed you can try providing minHeight & minWidth to the control as documented here https://www.w3schools.com/cssref/pr_dim_min-height.asp

expose a field in wpf user control for external use

I am very new to wpf and I would like to create a simple user control that consists of a textblock and a textbox so that I can reuse it. However, I do not really know how to bind the content of the textblock so that it can be set from outside and expose the textbox so that it could be bound to other field from the outside calling xaml.
The following is the code for my user control
<UserControl x:Class="WPFLib.UserControlLibs.TextBoxUsrCtrl"
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"
xmlns:local="clr-namespace:WPFLib.UserControlLibs"
mc:Ignorable="d"
d:DesignHeight="20"
d:DesignWidth="300">
<StackPanel Orientation='Horizontal'
Width='{Binding ActualWidth, ElementName=parentElementName}'
Height='{Binding ActualWidth, ElementName=parentElementName}'>
<Grid HorizontalAlignment='Stretch'>
<Grid.ColumnDefinitions>
<ColumnDefinition Width='1*' />
<ColumnDefinition Width='1*' />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Text='{Binding Text, ElementName=parentElementName}'
Background='Aqua'
Grid.Column='0'
Grid.Row='0' />
<TextBox x:Name='UserTxBox'
Grid.Column='1'
Grid.Row='0'
Background='Red'
HorizontalAlignment='Stretch'
Text='this is a test to see how it works' />
</Grid>
</StackPanel>
</UserControl>
How do I expose the Text from the TextBlock and TextBox so that it could be set and retrieved from the calling xaml?
For example
<Window x:Class="TestWPF.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:local="clr-namespace:TestWPF"
xmlns:controls='clr-namespace:WPFLib.UserControlLibs'
` mc:Ignorable="d"
Title="MainWindow"
Height="350"
Width="525"
WindowState='Maximized'
FontSize='18'>
<StackPanel>
<controls:TextBoxUsrCtrl Width='500' HorizontalAlignment='Left' **Text='NEED TO SET THE TEXT BLOCK HERE'**/>
</StackPanel>
</Window>
You should give it two dependency properties, one for each of the two text properties you want to expose (this is a horrible amount of boilerplate; I use Visual Studio's snippet feature to generate it all). Then in the UserControl XAML, you bind control properties to those.
public partial class TextBoxUsrCtrl : UserControl
{
public TextBoxUsrCtrl()
{
InitializeComponent();
}
#region Text Property
public String Text
{
get { return (String)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(nameof(Text), typeof(String), typeof(TextBoxUsrCtrl),
new FrameworkPropertyMetadata(null) {
// It's read-write, so make it bind both ways by default
BindsTwoWayByDefault = true
});
#endregion Text Property
#region DisplayText Property
public String DisplayText
{
get { return (String)GetValue(DisplayTextProperty); }
set { SetValue(DisplayTextProperty, value); }
}
public static readonly DependencyProperty DisplayTextProperty =
DependencyProperty.Register(nameof(DisplayText), typeof(String), typeof(TextBoxUsrCtrl),
new PropertyMetadata(null));
#endregion DisplayText Property
}
XAML. I simplified this so the layout works as I think you intended.
Note how the bindings use RelativeSource={RelativeSource AncestorType=UserControl} to bind to the dependency properties we defined above on the UserControl. By default, Binding will bind to properties of UserControl.DataContext, but we're not using that. The reason is that if we set UserControl.DataContext here, that will break the viewmodel property bindings in the final XAML fragment at the end of this answer. Those bindings will look for those properties on our control. There are workarounds but it gets ugly. The way I've done it here is best because it never breaks anybody's assumptions about DataContext inheritance.
<UserControl
x:Class="WPFLib.UserControlLibs.TextBoxUsrCtrl"
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"
xmlns:local="clr-namespace:WPFLib.UserControlLibs"
mc:Ignorable="d"
d:DesignHeight="20"
d:DesignWidth="300"
>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width='*' />
<ColumnDefinition Width='*' />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
Text='{Binding DisplayText, RelativeSource={RelativeSource AncestorType=UserControl}}'
Background='Aqua'
Grid.Column='0'
Grid.Row='0'
/>
<TextBox
x:Name='UserTxBox'
Grid.Column='1'
Grid.Row='0'
Background='Red'
HorizontalAlignment='Stretch'
Text='{Binding Text, RelativeSource={RelativeSource AncestorType=UserControl}}'
/>
</Grid>
</UserControl>
Usage in window, bound to viewmodel properties:
<local:TextBoxUsrCtrl
Text="{Binding TestText}"
DisplayText="{Binding ShowThisText}"
/>
Lastly, I'm not sure what you were getting at with ElementName=parentElementName. If that's meant to be a reference to a parent control, you can't do that, and it wouldn't be a good idea if you could. You wouldn't want a UserControl constrained by a requirement that a parent control must have a particular name. The answer to that requirement is simply that controls in XAML are only responsible for sizing themselves if they have a fixed size. If they should size to the parent, the parent is always the one responsible for that. So if you want to size two instances of TextBoxUsrCtrl to two different parents, that's fine. Each parent sizes its own children as it pleases.

Send commands between two usercontrols in WPF

I'm trying to send a command from one UserControl to another. The first one contains a button and the second one contains a custom class that derives from the Border class.
I want when I click the Button in UserControl to execute the Redraw method in CustomBorder in UserControl2.
Here is what I have done so far.
MainWindow.xaml:
<Window x:Class="SendCommands.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sendCommands="clr-namespace:SendCommands"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<sendCommands:ViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="50"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<sendCommands:UserControl1 Grid.Row="0"/>
<sendCommands:UserControl2 Grid.Row="1"/>
</Grid>
</Window>
UserControl1:
<UserControl x:Class="SendCommands.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Button Content="Redraw"
Width="200"
Height="30"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
</UserControl>
UserControl2:
<UserControl x:Class="SendCommands.UserControl2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sendCommands="clr-namespace:SendCommands">
<Grid>
<sendCommands:CustomBorder Background="Black">
</sendCommands:CustomBorder>
</Grid>
</UserControl>
CustomBorder class:
using System.Windows;
using System.Windows.Controls;
namespace SendCommands
{
public class CustomBorder : Border
{
public void Redraw()
{
// Operations to redraw some elements inside the CustomBorder
MessageBox.Show("We did it!");
}
}
}
ViewModel.cs:
namespace SendCommands
{
class ViewModel
{
}
}
Please somebody help me learn this once and for all. I'm new to MVVM concept and I have read a lot but no results. I really need a practical solution to get the concepts right.
What does your Redraw method is really supposed to do? Change border if some property has changed? E.g. an item in a shop was sold out?
In general view should reflect changes in the ViewModel. This happens automatically with bindings. View element, such as button, can communicate with ViewModel with Commands.
Therefore your button would look like this:
<Button Command={Binding ClickCommand} />
In your ViewModel you'll have a
public DelegateCommand ClickCommand {get; private set;}
and
ClickCommand = new DelegateCommand(ExecuteClick);
ExecuteClick would update some properties in the view model, e.g. if you have an online shop, set a SoldOut property of bike object to true.
Your view will in turn bind to properties of Bike and change its appearance if some properties change. Changes like text will happen by themselves, more complicated changes can be achieved with converters (e.g. change bckaground to red in SoldOut is true):
<Resources>
<SoldOutToBckgrConverter x:Key="soldOutToBckgrConverter" />
</Resources>
<Label Content={Binding Path=SelectedItem.Model} Background={Binding Path=SelectedItem.SoldOut, Converter={StaticResource soldOutToBckgrConverter}} />
SoldOutToBckgrConverter implements IValueConverter and converts True to Red.
Note: SelectedItem is again bound to a list, whose source is bound to sth like ObservableCollection on your ViewModel.
So basically you shouldn't call redraw, it should all redraw itself automatically with commands, changes in VM and bindings.
Update to your comment: that's what I tried to show, given that I understood the purpose of your redraw right. In my example with products and red background for sold items this will look like this:
In your VM:
public ObservableCollection<MyProduct> Products {get;set;}
private MyProduct selectedProduct;
public MyProduct SelectedProduct
{
get {return selectedProduct;}
set {
if (selectedProduct != value) {
selectedProducat = value;
RaisePropertyChanged(()=>SelectedProduct;
}
}
}
MyProduct has Model property (real world product model, i.e. brand) and SoldOut.
In your View:
<ListBox SelectedItem="{Binding SelectedProduct, Mode=TwoWay}" ItemsSource="{Binding Products}" >
<ListBox.ItemTemplate>
<Label Content={Binding Path=SelectedItem.Model} Background={Binding Path=SelectedItem.SoldOut, Converter={StaticResource soldOutToBckgrConverter}} />
</ListBox.ItemTemplate>
</ListBox>
Now when you click you button, VM changes SelectedProduct and Binding cahnges background (or border..)
You can use "CallMethodAction" behavior provided by Expression Blend. Add System.Windows.Interactivity.dll to your project and you can bind the method to event. In your case "ReDraw" method must bound to "Click" event. More information on this behavior.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<sendCommands:UserControl1 Grid.Row="0">
<i:Interaction.Triggers>
<i:EventTrigger EventName="RefreshButtonClick">
<ei:CallMethodAction MethodName="RedrawCustomBorder"
TargetObject="{Binding ElementName=customBorder}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</sendCommands:UserControl1>
<sendCommands:UserControl2 Grid.Row="1" x:Name="customBorder"/>
</Grid>

Categories

Resources