I have two UserControls in my application, both of them reside on the MainWindow. Now I need to interact between these two, so that when I click Button in UserControl1, the Text property of the TextBlock in UserControl2 changes.
How can I achieve this without MVVM? I would love to see the MVVM solution though if it is complete cause I'm totally new to it and it is very overwhelming.
MainWindow:
<Window x:Class="WpfApplication23.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"
xmlns:wpfApplication23="clr-namespace:WpfApplication23">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<wpfApplication23:UserControl1/>
<wpfApplication23:UserControl2 Grid.Row="1"/>
</Grid>
</Window>
UserControl1:
<UserControl x:Class="WpfApplication23.UserControl1"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Grid>
<Button Content="Change Text"
Width="200"
Height="80"
Click="ButtonBase_OnClick"/>
</Grid>
</UserControl>
UserControl2:
<UserControl x:Class="WpfApplication23.UserControl2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<TextBox Width="100" Height="20"/>
</Grid>
</UserControl>
Click Event for Button:
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
// Access the TextBlock in UserControl2 and Change its Text to "Hello World"
}
To do this without MVVM, you will need to do the following:
Set up an event like "UpdateText" on the first user control, have the button's click method raise this event.
public event Action<string> UpdateText;
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
// Access the TextBlock in UserControl2 and Change its Text to "Hello World"
if (UpdateText != null)
UpdateText("HelloWorld");
}
Listen to the event in MainWindow that then calls a function on the second user control to update the textblock. Something like:
public MainWindow()
{
InitializeComponent();
myUserControl1.UpdateText += HandleUpdateText;
}
private void HandleUpdateText(String newText)
{
myUserControl2.SetText(newText);
}
Now the right answer to do this would be to use MVVM, but the code sample required would be too long for StackOverflow. I will provide the steps however:
Set up a DependencyProperty on the second user control for the "Text" property, and bind to it:
<UserControl x:Class="WpfApplication23.UserControl2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<TextBox Width="100" Height="20" Text="{Binding ControlText}"/>
</Grid>
</UserControl>
Put an ICommand dependency property on the first user control that will be invoked on the button click. Bind a function to it on MainWindow that will set the ControlText property to the parameter object.
Probably not the best "First MVVM" sample, as it requires a few advanced concepts (commanding and dependency properties), but it shouldn't be too hard to implement.
Related
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
i have a UserControl that contains a TextBox now i am loading another UserControl that contains a TextBlock .When the button is clicked, I want to assign value entered in TextBox to TextBlock of another control that is loaded. How can i do this ?
Main UserControl
<UserControl x:Class="IntelliVentory.UserControls.CategoryControl"
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:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="670" d:DesignWidth="1100">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<TextBox Grid.Row="0" Name="CategoryNameBox" Width="350" />
<Button Grid.Row="1" Click="AddCategoryFunc">Load Another Control</Button>
<Grid Grid.Row="2" Name="CategoriesWraper"></Grid>
</Grid>
</UserControl>
Another UserControl
<UserControl x:Class="IntelliVentory.UserControlModules.CategoryModule"
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:IntelliVentory.UserControlModules"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<TextBlock Name="CategoryName" FontSize="12" FontWeight="Thin">Category Name Here</TextBlock>
</Grid>
</UserControl>
Main UserControl.cs
Loading another UserControl.
private void AddCategoryFunc(object sender, RoutedEventArgs e)
{
UserControl categoryMod = new CategoryModule();
CategoriesWraper.Children.Add(categoryMod);
}
You want to have something like
categoryMod.CategoryNameValue = categoryControl.CategoryNameValue;
So you need to define two properties, one CategoryNameValue property with which you get the Text value of the TextBox, and one CategoryNameValue property with which you can set the Text property of the TextBlock.
Define this property in the CategoryControl class,
public string CategoryNameValue { get { return CategoryNameBox.Text; }
And this in CategoryModule class,
public string CategoryNameValue { set { CategoryName.Text = value; }
And you can start using them in your code.
You can define them as Dependency Properties instead of plain CLR properties, and then look to use data binding. With data binding both user controls can be bound to the same data model so their values are synced automatically.
Edit:
Turns out you can access a UserControl's child elements from outside as if they are public fields. That is, you can write code like this without having to define new properties
CategoryModule categoryMod = new CategoryModule();
categoryMod.CategoryName.Text = CategoryNameBox.Text;
CategoriesWraper.Children.Add(categoryMod);
I have a WPF window with a grid containing 4 rectangles. One of these has a <frame> for showing pages, which is used in my application. I want to add commands to these buttons on my windows as well as to the pages.
Things I use: MVVM, Window as Mainwindow and Pages as contentpublisher in a frame.
For example, I want to login applicationwide with a button and command. While doing this on the page in my frame there are no errors, but I can't do the same in the window.
I was wondering if the windows lose focus so it can't fire that event while navigating to a page in the frame. So I tried to get the window with following command binding:
<Button Content="" FontFamily="Segoe UI Symbol"
Command="{Binding CommandWhichDoesNotFire, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type vw:MainViewModel}}}"
Width="32">
In my ViewModel "MainViewModel" I have a public ICommand Property and I initialize it at the constructor:
public ICommand CommandWhichDoesNotFire;
public MainViewModel()
{
MessageBox.Show("VM is real");
CommandWhichDoesNotFire= new TestCommand();
}
The DataContext of my MainView is set in the behind-code BEFORE InitilizeComponents();
Clicking on the button does not start ANY call of my command. It simply does not fire at all. What am I missing guys?
You should have :
public ICommand CommandWhichDoesNotFire{get;set;}
public MainViewModel()
{
MessageBox.Show("VM is real");
CommandWhichDoesNotFire= new TestCommand(MyCommand);
}
private void MyCommand(object obj){
//Whatever you want to do
}
I think I have found a solution to your problem.
The Frame for some reason doesn't inherit the DataContext of it's parent, even if you set the DataContext explicitly like so:
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}"
it still doesn't work. What it does it only sets the DataContext of the frame but not child elements:
And here is the DataContext of a child element:
Now this made me think that whatever we have always loved about WPF and it's controls was the ability to inherit the DataContext from it's parent, with exception of ContextMenu and now the Frame. Here is the approach I took when I had a frist look at oyur problem:
<Window x:Class="WpfApplication1.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:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainVM/>
</Window.DataContext>
<Window.Resources>
<!--<local:MainVM x:Key="mainVM"/>-->
<local:LoginPage x:Key="login" />
<!--DataContext="{StaticResource mainVM}"-->
<ControlTemplate x:Key="ctrlTmpl">
<local:LoginPage/>
</ControlTemplate>
</Window.Resources>
<Grid>
<!--<Button x:Name="button" Content="Do something" Click="btnDoSomething" HorizontalAlignment="Left" Margin="221,60,0,0" VerticalAlignment="Top" Width="75"/>-->
<!--<Control Template="{StaticResource ctrlTmpl}"/> This works-->
<Frame Content="{StaticResource login}" DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" />
</Grid>
Then I thought you can do this another way:
<Window x:Class="WpfApplication1.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:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<!--<Window.DataContext>
<local:MainVM/>
</Window.DataContext>-->
<Window.Resources>
<local:MainVM x:Key="mainVM"/>
<local:LoginPage x:Key="login" DataContext="{StaticResource mainVM}"/>
<!---->
<ControlTemplate x:Key="ctrlTmpl">
<local:LoginPage/>
</ControlTemplate>
</Window.Resources>
<Grid>
<!--<Button x:Name="button" Content="Do something" Click="btnDoSomething" HorizontalAlignment="Left" Margin="221,60,0,0" VerticalAlignment="Top" Width="75"/>-->
<!--<Control Template="{StaticResource ctrlTmpl}"/> This works-->
<Frame Content="{StaticResource login}"/>
</Grid>
Notice how I included the VM in the resources and then used that instance to be the Controls DataContext. At this point when I click the button in my LoginPage.xaml which by the way is a UserControl it triggers the Command located in my MainVM. At this point you would have to assign the Window DataContext in the code behind like so:
public MainWindow()
{
InitializeComponent();
var vm = this.TryFindResource("mainVM");
if(vm != null)
{
this.DataContext = vm;
}
}
Now at this point you can use some sort of triggers to navigate through pages and use different Pages or UserControls. HTH
P.S. When I get a chance I will update some information about Context Menu and Frame from MSDN. Happy Coding
I made following application (as a test)
XAML:
<Window x:Class="GUITest.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">
<Grid Background="Transparent">
<TextBlock Text="openDialog" Background="Red" HorizontalAlignment="Center" VerticalAlignment="Top" MouseDown="TextBlock_MouseDown" />
</Grid>
</Window>
C#:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void TextBlock_MouseDown(object sender, MouseButtonEventArgs e)
{
OpenFileDialog dlg = new OpenFileDialog();
Nullable<bool> result = dlg.ShowDialog();
if (result == true)
{
Console.Out.WriteLine(dlg.FileName);
}
}
}
I catch the mouseDown event because it catches both mouse button down events and touch down events. The code has the expected behavior for mouse clicks. Touch gives me some trouble.
If i touch the TextBlock it opens a dialog window as asked. after closing it, any touch on the window opens the dialog window, even if the touch was not on the TextBlock.
Is this a bug? Can i work around this?
EDIT: i posted a workaround, an actual fix would still be usefull
For other people who run into the same problem. This isn't a fix to the problem, but it is a workaround.
I used a button and restyled it to look like a TextBlock
XAML:
<Grid Background="Transparent">
<Button HorizontalAlignment="Left" VerticalAlignment="Top" Click="Button_Click" Content="openDialog">
<Button.Template>
<ControlTemplate TargetType="Button">
<ContentPresenter />
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
Code for Button_Click is the same as TextBlock_MouseDown
I have created a basic application using Prism & MVVM. So far, it only consists of the Shell, and one View/ViewModel.
During application load, I am loading the View into my main region and this displays on screen. This works, but I cannot get the textbox on the view to focus. It looks like the cursor is in the box (although it's not flashing), but it doesn't accept text input until I click on the textbox.
I've recreated this in a new project, where all I've done is install prism/prism.unityextensions, set up the shell and the view, and loaded the view into the shell region. Neither xaml file has anything in the code behind.
Shell
<Window x:Class="MVVMFocusTest.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://www.codeplex.com/prism"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DockPanel LastChildFill="True">
<ContentControl Name="MainRegion" DockPanel.Dock="Top" prism:RegionManager.RegionName="MainRegion" />
</DockPanel>
</Grid>
</Window>
View1
<UserControl x:Class="MVVMFocusTest.View1"
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">
<StackPanel>
<Grid FocusManager.FocusedElement="{Binding ElementName=Username}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0">Username</Label>
<TextBox Name="Username" Grid.Row="0" Grid.Column="1" ToolTip="Enter Username" TabIndex="0" />
<Label Grid.Row="1" Grid.Column="0">Password</Label>
<PasswordBox Grid.Row="1" Grid.Column="1" Name="LoginPassword" PasswordChar="*" ToolTip="Enter Password" TabIndex="1" />
</Grid>
</StackPanel>
</UserControl>
Can anyone point out what I'm doing wrong? As far as I'm aware, the FocusManager.FocusedElement="{Binding ElementName=Username}" should be sufficient to set the focus.
As per FocusManager documentation -
Logical focus pertains to the FocusManager.FocusedElement within a
specific focus scope.
So, its not necessary that element with logical focus will have keyboard focus as well but vice versa is true i.e. element with keyboard focus will surely have a logical focus as well.
As stated in documentation FocusManager.FocusedElement guarantees logical focus and not keyboard focus. So what you can do is create an attach behaviour similar to FocusManager.FocusedElement which will set keyboard focus on an element.
You can refer to this for setting keyboard focus using attached behaviour - Setting keyboard focus in WPF.
Code from that article -
namespace Invoices.Client.Wpf.Behaviors
{
using System.Windows;
using System.Windows.Input;
public static class KeyboardFocus
{
public static readonly DependencyProperty OnProperty;
public static void SetOn(UIElement element, FrameworkElement value)
{
element.SetValue(OnProperty, value);
}
public static FrameworkElement GetOn(UIElement element)
{
return (FrameworkElement)element.GetValue(OnProperty);
}
static KeyboardFocus()
{
OnProperty = DependencyProperty.RegisterAttached("On", typeof(FrameworkElement), typeof(KeyboardFocus), new PropertyMetadata(OnSetCallback));
}
private static void OnSetCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var frameworkElement = (FrameworkElement)dependencyObject;
var target = GetOn(frameworkElement);
if (target == null)
return;
frameworkElement.Loaded += (s, e) => Keyboard.Focus(target);
}
}
}
Use in XAML -
<UserControl xmlns:behaviors="clr-namespace:Invoices.Client.Wpf.Behaviors">
<Grid behaviors:KeyboardFocus.On="{Binding ElementName=TextBoxToFocus}">
<TextBox x:Name="TextBoxToFocus" />
</Grid>
</UserControl>
FocusManager.FocusedElement="{Binding ElementName=Username}" sets logical focus but not physical focus.
Physical focus is the normal focus, logical focus is kinda a second focus which is still a little bit buggy in wpf 4.0.
I would suggest you to use Keyboard.Focus(this.Username).