I'm learning WPF with MVVM pattern. My app is counting Body Mass Index, so it's really simple - just to help me understand the foundations of this pattern.
I was experimenting a little bit and decided to implement TextChanged event via Commands to allow user see changes in overall BMI label while he's typing a height or weight.
My textBoxes in which I use the TextChanged command are binded to ViewModel properties in TwoWay mode, so I thought that if I raise INotifyPropertyChanged event on properties binded to these textBoxes when TextChanged event occurs it will automatically update View, but it doesn't.
So question is, what am I doing wrong and how can I implement it properly?
PS. Everything else excepting View update is working (command is used, I checked with breakpoint it just doesn't change the View)
Thanks in advance
CustomCommand class:
public class CustomCommand : ICommand
{
Action<object> action;
Predicate<object> predicate;
public CustomCommand(Action<object> execute, Predicate<object> canExecute)
{
action = execute;
predicate = canExecute;
}
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
}
remove
{
CommandManager.RequerySuggested -= value;
}
}
public bool CanExecute(object parameter)
{
if (predicate(parameter))
return true;
else
return false;
}
public void Execute(object parameter)
{
action(parameter);
}
}
One of two textBoxes:
<TextBox HorizontalAlignment="Left" Height="23" Margin="148,83,0,0" TextWrapping="Wrap" Text="{Binding Person.Weight, Mode=TwoWay}" VerticalAlignment="Top" Width="76">
<i:Interaction.Triggers>
<i:EventTrigger EventName="TextChanged">
<i:InvokeCommandAction Command="{Binding Path=textChangedCommand}"></i:InvokeCommandAction>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
And ViewModel, where TextChanged method is passed to a command
public class MainWindowViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler propertyChanged = PropertyChanged;
if (propertyChanged != null)
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public ICommand textChangedCommand { get; set; }
//public List<float> BMI_Changed;
private PersonInfo person;
public PersonInfo Person
{
get
{
return person;
}
set
{
person = value;
OnPropertyChanged("Person");
}
}
public MainWindowViewModel()
{
//BMI_Changed = new List<float>();
textChangedCommand = new CustomCommand(TextChanged, CanBeChanged);
person = Data.personInfo;
}
private void TextChanged(object obj)
{
OnPropertyChanged("BMI");
OnPropertyChanged("Weight");
OnPropertyChanged("Height");
}
private bool CanBeChanged(object obj)
{
return true;
}
}
Rest of my View code, for general overview:
<Window x:Class="SportCalculators_MVVM.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:SportCalculators_MVVM"
xmlns:enum="clr-namespace:SportCalculators_MVVM.Model"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
mc:Ignorable="d"
Title="MainWindow" Height="340.278" Width="260.256" Loaded="Window_Loaded"
DataContext="{Binding Source={StaticResource viewModelLocator}, Path=mainWindowViewModel}">
<Grid x:Name="grid">
<Slider x:Name="mass" HorizontalAlignment="Right" Margin="0,128,58,0" VerticalAlignment="Top" Width="155" Value="{Binding Person.Weight, Mode=TwoWay}" Maximum="150" Minimum="20"/>
<Slider x:Name="height" HorizontalAlignment="Left" Margin="40,210,0,0" VerticalAlignment="Top" Width="155" Minimum="100" Maximum="230" Value="{Binding Person.Height, Mode=TwoWay}"/>
<RadioButton x:Name="sex" Content="Kobieta" HorizontalAlignment="Left" Margin="45,41,0,0" VerticalAlignment="Top" IsChecked="{Binding Person.Sex, Converter={StaticResource ResourceKey=genderConverter}, ConverterParameter={x:Static enum:Sex.Female}}"/>
<RadioButton x:Name="sex1" Content="Mężczyzna" HorizontalAlignment="Left" Margin="150,41,0,0" VerticalAlignment="Top" IsChecked="{Binding Person.Sex, Converter={StaticResource ResourceKey=genderConverter}, ConverterParameter={x:Static enum:Sex.Male}}"/>
<Label x:Name="massLabel" Content="Waga" HorizontalAlignment="Left" Margin="40,80,0,0" VerticalAlignment="Top"/>
<Label x:Name="heightLabel" Content="Wzrost" HorizontalAlignment="Left" Margin="39,167,0,0" VerticalAlignment="Top"/>
<Label x:Name="label" Content="{Binding Person.BMI}" HorizontalAlignment="Left" Margin="39,274,0,0" VerticalAlignment="Top"/>
<Button Content="Statystyki" HorizontalAlignment="Left" Margin="149,274,0,0" VerticalAlignment="Top" Width="75" RenderTransformOrigin="0.325,-0.438"/>
<TextBox HorizontalAlignment="Left" Height="23" Margin="148,83,0,0" TextWrapping="Wrap" Text="{Binding Person.Weight, Mode=TwoWay}" VerticalAlignment="Top" Width="76">
<i:Interaction.Triggers>
<i:EventTrigger EventName="TextChanged">
<i:InvokeCommandAction Command="{Binding Path=textChangedCommand}"></i:InvokeCommandAction>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
<TextBox HorizontalAlignment="Left" Height="23" Margin="148,170,0,0" TextWrapping="Wrap" Text="{Binding Person.Height, Mode=TwoWay}" VerticalAlignment="Top" Width="76">
<i:Interaction.Triggers>
<i:EventTrigger EventName="TextChanged">
<i:InvokeCommandAction Command="{Binding Path=textChangedCommand}"></i:InvokeCommandAction>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
</Grid>
Ed Plunkett gave the simplest solution:
There is no need to write whole bunch of code to implement command while TextChanged occurs, there is a Binding property UpdateSourceTrigger which determines when there should be the update, by default it's set to LostFocus so it is for example when you click on another control, if you'd like to update it while user is typing, you need to set value to PropertyChanged and that's it!
<TextBox Text="{Binding Person.Weight, UpdateSourceTrigger=PropertyChanged}">
Related
I have run into a scenario where I have to update the source data object in an WPF project WITHOUT any code behind (strictly XAML changes only). The description is as follow: I have two text boxes, calling them Name and Title of an Employee.
Employee.cs:
public class Employee : INotifyPropertyChanged
{
private string name;
public string Name {
get { return name; }
set
{
name = value;
if(PropertyChanged != null)
{
PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Name"));
}
}
}
private string title;
public string Title {
get { return title; }
set
{
title = value;
if (PropertyChanged != null)
{
PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Title"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
public static Employee GetEmployee()
{
var emp = new Employee()
{
Name = "Ali Ahmed",
Title = "Developer"
};
return emp;
}
}
The windows class is as follow:
public partial class MainWindow : Window
{
Employee employee = Employee.GetEmployee();
public MainWindow()
{
InitializeComponent();
DataContext = employee;
}
private void OnNameChanged(object sender, RoutedEventArgs e)
{
TextBox castedTextBox = sender as TextBox;
employee.Title = castedTextBox.Text;
}
}
Notice the OnNameChanged handler will set the employee's title to a string of his name (simulating an unwanted bug in actual project)
XAML code:
<Window x:Class="WpfApp1.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:WpfApp1"
xmlns:i = "http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei = "http://schemas.microsoft.com/expression/2010/interactions"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<CheckBox x:Name="checkBox" Content="CheckBox" HorizontalAlignment="Left" Height="21" Margin="59,39,0,0" VerticalAlignment="Top" Width="89"/>
<ComboBox HorizontalAlignment="Left" Height="25" Margin="59,72,0,0" VerticalAlignment="Top" Width="179" />
<TextBox HorizontalAlignment="Left" x:Name="TitleTextBox" Height="21" Margin="60,113,0,0" TextWrapping="Wrap" Text="{Binding Title, Mode=TwoWay}" VerticalAlignment="Top" Width="178">
<i:Interaction.Triggers>
<i:EventTrigger EventName="TextChanged">
<ei:ChangePropertyAction TargetName="TitleTextBox" TargetObject="{Binding ElementName=TitleTextBox}" PropertyName="Text" Value=""/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
<TextBox HorizontalAlignment="Left" Height="24" Margin="60,146,0,0" TextWrapping="Wrap" Text="{Binding Name, Mode=TwoWay}" TextChanged="OnNameChanged" VerticalAlignment="Top" Width="178"/>
<TextBlock HorizontalAlignment="Left" Height="27" Margin="59,180,0,0" TextWrapping="Wrap" Text="{Binding Name, Mode=OneWay}" VerticalAlignment="Top" Width="179"/>
<TextBlock HorizontalAlignment="Left" Height="27" Margin="60,221,0,0" TextWrapping="Wrap" Text="{Binding Title, Mode=OneWay}" VerticalAlignment="Top" Width="179"/>
</Grid>
</Window>
The only important thing about this XAML code is the usage of the interaction trigger on TitleTextBox. The intent and purpose of this block of interaction XAML code is to wipe out the title text box in hope that the TwoWay binding will also wipe out the value of the employee's title that was wrongly set the the OnNameChanged event above. However, it only wipes out the content of the box without actually updating the model object employee underneath.
My question: Is there something I could do on XAML alone (since this is a specific constraint I have to work with) that could force the model to recognize the changes in the Title box and update the Employee object?
I have following code:
<ListBox x:Name="listbox1" HorizontalAlignment="Left" Height="240" Margin="81,80,0,0" VerticalAlignment="Top" Width="321" BorderBrush="#FF6C6C6C" SelectionMode="Single"/>
<ListBox x:Name="listbox2" HorizontalAlignment="Left" Height="240" Margin="482,80,0,0" VerticalAlignment="Top" Width="318" BorderBrush="#FF6C6C6C" SelectionMode="Multiple"/>
<Button x:Name="uButton" Content="Upload stuff" HorizontalAlignment="Left" Margin="840,178,0,0" VerticalAlignment="Top" Width="160" Height="46" BorderBrush="#FF6C6C6C" Foreground="#FF0068FF" Click="ButtonClick">
...
</Button>
I want the button uButton to be disabled by using IsEnable = false, until the user selected one Item from listbox1 and one or more Items from listbox2.
How can I achieve this?
Providing you use the MVVM pattern (which you should with WPF), you should implement an ICommand and bind it to the Command Property of your button. In the CanExecute method of your button you can check the Count of the selected Items of your ListBoxes. It automatically enables/disables your button when the criteria are met. This could look something like this:
public class SomeCommand: ICommand
{
#region Fields
MainWindow mainWindow;
#endregion
#region Constructors and Destructors
public SomeCommand( MainWindow mw )
{
this.mainWindow = mw;
}
#endregion
#region ICommand
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute( object parameter )
{
return ( this.mainWindow.listbox1.SelectedItems.Count != 0
&& this.mainWindow.listbox2.SelectedItems.Count != 0 );
}
public void Execute( object parameter )
{
//DO STUFF;
}
#endregion
}
And in your XAML:
<ListBox x:Name="listbox1" HorizontalAlignment="Left" Height="240" Margin="81,80,0,0" VerticalAlignment="Top" Width="321" BorderBrush="#FF6C6C6C" SelectionMode="Single"/>
<ListBox x:Name="listbox2" HorizontalAlignment="Left" Height="240" Margin="482,80,0,0" VerticalAlignment="Top" Width="318" BorderBrush="#FF6C6C6C" SelectionMode="Multiple"/>
<Button x:Name="uButton" Command="{Binding SomeCommand}" Content="Upload stuff" HorizontalAlignment="Left" Margin="840,178,0,0" VerticalAlignment="Top" Width="160" Height="46" BorderBrush="#FF6C6C6C" Foreground="#FF0068FF" />
Add SelectionChanged="ListBox_SelectionChanged" into your listbox1 and listbox2 properties in your xaml code.
add IsEnabled="False" into your buttons properties
then in your code
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (listbox1.SelectedItem != null && listbox2.SelectedItems != null)
ubutton.IsEnabled = true;
else
ubutton.IsEnabled = false;
}
I have a Textbox and for that textbox I have attached a keydown event. Everything is working fine but I just noticed that when i'm pressing the 'Backspace' and 'Delete' Key, the binding command is not being called.
My View xaml file :-
<TextBox x:Name="textBox" Width="500" Text="{Binding TextBoxText, UpdateSourceTrigger=PropertyChanged}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="KeyDown">
<cmd:EventToCommand Command="{BindingPath=TextBoxKeyDownEvent}" PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
My ViewModel cs file :-
//TextBox Key Down Event Handler
private DelegateCommand _textBoxKeyDownEvent;
public ICommand TextBoxKeyDownEvent
{
get
{
if (_textBoxKeyDownEvent == null)
{
_textBoxKeyDownEvent = new DelegateCommand(TextBoxKeyDownEventHandler);
}
return _textBoxKeyDownEvent;
}
set { }
}
Can somebody give me some suggestion
EDIT:
You have to use PreviewKeyDown the it works. KeyDown is not fired on Space and Delete. If you ignore MVVM and put the handler of KeyDown in codebehind it will also fail.
How about binding the Text-Property to a string in you viewmodel?
I build a fast, simple example of my idea.
Result
Text from the TextBox on the left side is simply populated to the Textblock on the right side.
View
<Window x:Class="WpfApplication1.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">
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding TextBoxValue, UpdateSourceTrigger=PropertyChanged}" Width="250"/>
<StackPanel Orientation="Horizontal">
<TextBlock>"</TextBlock>
<TextBlock Text="{Binding TextBoxValue, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock>"</TextBlock>
</StackPanel>
</StackPanel>
</Window>
ViewModel
public class MainWindowViewModel : INotifyPropertyChanged
{
private string textBoxValue;
public string TextBoxValue
{
get { return textBoxValue; }
set
{
textBoxValue = value;
OnTextBoxValueChanged();
RaisePropertyChanged();
}
}
void OnTextBoxValueChanged()
{
// you logic here, if needed.
}
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
you most use PreviewKeyDown event.
Like this:
<EventSetter Event="PreviewKeyDown" Handler="TextBox_PreviewKeyDown"/>
Edit: You are correct - the default behavior is not executed. You should use ec8ors solution, which is much better anyway:
<TextBox x:Name="textBox" Width="500" Text="{Binding TextBoxText, UpdateSourceTrigger=PropertyChanged}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewKeyDown">
<i:InvokeCommandAction Command="{Binding TextBoxKeyDownEvent, Mode=OneWay}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
Original:
You can use InputBindings to call your command when "special" keys have been pressed:
<TextBox x:Name="textBox" Width="500" Text="{Binding TextBoxText, UpdateSourceTrigger=PropertyChanged}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="KeyDown">
<cmd:EventToCommand Command="{BindingPath=TextBoxKeyDownEvent}" PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
<TextBox.InputBindings>
<KeyBinding Command="{Binding TextBoxKeyDownEvent}" Key="Delete" />
<KeyBinding Command="{Binding TextBoxKeyDownEvent}" Key="Back" />
</TextBox.InputBindings>
</TextBox>
I am developing my first Windows 8 app, in one page i am trying to update button text with latest timestop when page loads. I defined my xaml and codebehind like below:
I am using databinding to update the button text but it is not working as expected:
MainPage.xaml
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<Button HorizontalAlignment="Left" Margin="333,284,0,0" VerticalAlignment="Top" Height="69" Width="162">
<Button.Resources>
<DataTemplate x:Key="DataTemplate1">
<Grid>
<TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="{Binding ButtonText}" VerticalAlignment="Top" Foreground="#FFFF6800" Height="34" Margin="-30,0,-22,-14" Width="115"/>
</Grid>
</DataTemplate>
</Button.Resources>
<Button.ContentTemplate>
<StaticResource ResourceKey="DataTemplate1"/>
</Button.ContentTemplate>
</Button>
</Grid>
MainPage.xaml.cs
public StatsClass Stats { get; private set; }
public MainPage()
{
this.InitializeComponent();
this.DataContext = Stats;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
UpdateButton();
}
private void UpdateButton()
{
if (Stats == null)
Stats = new StatsClass();
Stats.ButtonText = DateTime.Now.ToString();
}
StatsClass.cs
public class StatsClass : INotifyPropertyChanged
{
private string _buttonText;
public string ButtonText
{
get
{
return _buttonText;
}
set
{
_buttonText = value;
OnPropertyChanged("ButtonText");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
You have set Content of your Button twice, once with Content="Button" and again with.Button.ContentTemplate. You could just have:
<Button HorizontalAlignment="Left" Margin="333,284,0,0" VerticalAlignment="Top" Height="69" Width="162">
<Grid>
<TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="{Binding ButtonText}" VerticalAlignment="Top" Foreground="#FFFF6800" Height="34" Margin="-30,0,-22,-14" Width="115"/>
</Grid>
</Button>
I had a similar issue yesterday using binding in a DataTemplate. I guess that you also had a binding error in the debug output.
I solved it using a relative source like that:
<TextBlock Text={Binding DataContext.ButtonText,
RelativeSource={RelativeSource FindAncestor, AncestorType=*YourControl*}}"/>
The Template has no direct access to the datacontext. By using the relative source you can bind to its properties.
I have a stackpanel with image and button in it. I want to fire event when user clicks on a button in stackPanel. My code in xaml is
<StackPanel x:Uid="TemperatureMonitor" Orientation="Horizontal" HorizontalAlignment="Left" ToolTip="{DynamicResource InstrumentZweiMesswert}" Height="35">
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewMouseLeftButtonDown">
<ei:CallMethodAction TargetObject="{Binding}" MethodName="OnAddUserControl"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<Image Width="35" Height="35" x:Uid="Image_15" Source="/Resources\png\TemperatureMonitor.png"/>
<Button x:Uid="TemperatureMonitor" Content="Temperatur Monitor" x:Name="TemperatureMonitor" IsEnabled="True" Width="135"/>
</StackPanel>
And method OnAddUserControl in my viewModel is
public void OnAddUserControl(object sender, RoutedEventArgs e)
{
//some code
}
The problem it that I don't get into OnAddUserControl. Any ideas why?
I want to fire this event when user makes leftMouseClick on a button. So I don't know why, but RelayCommand also doesn't help and not fires method OnAddUserControl. When I moved iteraction code to my button and it looks like this :
<StackPanel Background="Black" x:Uid="TemperatureMonitor" Orientation="Horizontal" HorizontalAlignment="Left" ToolTip="{DynamicResource InstrumentZweiMesswert}" Height="35">
<Image Width="35" Height="35" x:Uid="Image_15" Source="/Resources\png\TemperatureMonitor.PNG"/>
<Button x:Uid="TemperatureMonitor" Content="Temperatur Monitor" x:Name="TemperatureMonitor" IsEnabled="True" Width="135" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewMouseLeftButtonDown">
<ei:CallMethodAction TargetObject="{Binding}" MethodName="OnAddUserControl"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</StackPanel>
i've get during runtime mistake that says "For object Type"DockSite" cannot find methodname "OnAddUserControl"". I will appreciate any ideas or help
You can use RelayCommand for this purpose.
Add RelayCommand.cs to your project.
class RelayCommand : ICommand
{
private Action<object> _action;
public RelayCommand(Action<object> action)
{
_action = action;
}
#region ICommand Members
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
if (parameter != null)
{
_action(parameter);
}
else
{
_action("Hello World");
}
}
#endregion
}
And this is your ViewModel. I called this MainWindowViewModel. So, add MainWindowViewModel.cs class to your solution.
class MainWindowViewModel
{
private ICommand m_ButtonCommand;
public ICommand ButtonCommand
{
get
{
return m_ButtonCommand;
}
set
{
m_ButtonCommand = value;
}
}
public MainWindowViewModel()
{
ButtonCommand=new RelayCommand(new Action<object>(ShowMessage));
}
public void ShowMessage(object obj)
{
MessageBox.Show(obj.ToString());
}
}
And this is your xaml:
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<StackPanel>
<Button Width="220" Content="Click me" Command={Binding ButtonCommand} CommandParameter="StackOverflow" />
</StackPanel>
It will show you messageBox after clicking button. So you change your project for handing Button Click event in this way.