I'm having a problem where user input is interrupted by updates from the bound ViewModel.
The ViewModel exposes a numerical value which changes each second. This is bound to a TextBox in the view. We want the TextBox to display this value, which it does well.
However, when the user clicks on the TextBox and tries to enter a new value, the value in the model gets updated which causes the user-entered value in the textbox to be overwritten.
How can I most easily solve this issue, supporting user input and regular updates in the same control? I would appreciate code examples (C# / XAML).
If you need more details just ask :)
When TextBox gets focus (IsFocused property equals true) I change binding type so that it would not be updated unless it looses focus.
ViewModel:
class MainViewModel : INotifyPropertyChanged
{
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
Debug.WriteLine(value);
OnPropertyChanged();
}
}
public MainViewModel()
{
Task.Factory.StartNew(async () =>
{
for (int i = 0; i < 1000; i++)
{
Name = i.ToString();
await Task.Delay(3000);
}
});
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
XAML:
<StackPanel>
<TextBox>
<TextBox.Style>
<Style TargetType="TextBox">
<Setter Property="Text" Value="{Binding Name, Mode=OneWay}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsFocused}" Value="True">
<Setter Property="Text" Value="{Binding Name, Mode=OneWayToSource, UpdateSourceTrigger=LostFocus}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<Button Content="Click"/>
</StackPanel>
I got sequence 2,3 then I inputed 56 and clicked Button so textbox lost focus and sent value to Name property. Debug printed following sequence:
1
2
3
56
4
Related
I am trying to make a ComboBox where the drop-down list shows the name and description of the campaign elements (properties NombreCampana and Descripcion of class Campana) and when you select one value it shows the Name (NombreCampana). I have done this with an ObservableCollection so the changes affect the ComboBox without reloading.
In the MainMenu window (menuPrincipal), I have this XAML for the ComboBox:
<ComboBox Style="{DynamicResource comboBoxCampaign}"
x:Name="campaignComboBox"
Text="{DynamicResource CampaignList}"
ItemsSource="{Binding Path=Nombres}"
SelectedValue="{Binding Path=NombreCampana, Mode=TwoWay}"
SelectedValuePath="Value"
SelectionChanged="CampaigneComboBox_SelectionChanged">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}
{1}">
<Binding Path="NombreCampana"/>
<Binding Path="Descripcion"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
The ComboBox, is linked to an ObservableCollection in the code-behind:
private List<Campana> listacampana = new List<Campana>();
private ObservableCollection<Campana> nombres = new ObservableCollection<Campana>();
public ObservableCollection<Campana> Nombres
{
get { return nombres; }
set { nombres = value; }
}
public List<Campana> ListaCampanas
{
get { return listacampana; }
set { listacampana = value; }
}
public menuPrincipal()
{
InitializeComponent();
ConfiguracionPagina.DefinirIdioma(this, "MainMenu");
this.listacampana.AddRange(MainWindow.DatosUsuario.ListCampaigns);
foreach (Campana item in this.listacampana)
{
this.nombres.Add(item);
}
campaignComboBox.ItemsSource = this.nombres;
}
That code defines the language, changes the texts, adds the user list to the property list, and after that adds to the all the items Campana (campaign) to the ObservableCollection.
After that I set the ItemSource of the ComboBox to Nombres (the ObservableCollection).
The class Campana is:
public class Campana
{
private string nombre;
private string descripcion;
private string imagen;
private List<RecursosCampana> listarecursos = new List<RecursosCampana>();
public string DireccionImagen
{
get { return imagen; }
set { imagen = value; }
}
public List<RecursosCampana> RecursosCampana
{
get { return listarecursos; }
set { listarecursos = value; }
}
public string Descripcion
{
get { return descripcion; }
set { descripcion = value; }
}
public string NombreCampana
{
get { return nombre; }
set { nombre = value; }
}
}
When I run the application, the DataTemplate works fine and shows the items with the description.
But when I select one item I get this:
I have tried everything that I founded by searching Google, but I don't get what is happening. Also, if I don't define the ItemSource in the code, the XAML ItemSource doesn't work. This is what happens if I comment out campaignComboBox.ItemsSource = this.nombres;:
Edit: answering the comments, i have solved thanks to #Keith Stein, a part of the problem, this was the style
<Style x:Key="comboBoxCampaign" TargetType="ComboBox">
<Setter Property="Margin" Value="10"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="IsEditable" Value="True"/>
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="Focusable" Value="True"/>
<Setter Property="FontSize" Value="15"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="Height" Value="Auto"/>
</Style>
I used that to have a "placeholder" text, an initial text which is going to dissapears once you click in the combobox, when i delete "IsEditable" i lost the initial title the combobox just have the V to press, but now when i select an item, i get the name + description.
Is not exactly what i want, i just want to show the name, but is more close to that.
Edit2: The problem with the databinding was that i didnt use the DataContext = this; now i can get the bind from xaml but still i cant show just the name.
Lastly answering to the last comment, what i am trying to do is:
- I'm doing an application to prepare roleplaying adventures or scenarios, in this section the user select the campaign that he is going to use, so i want that when he press the combobox, he can see the name of the campaign and the description because he could have 2 similar campaigns or based in the same system, and once he choose one, just show the name that is more elegant specially if he writes a big description
Finally i get the answer in this post Can I use a different Template for the selected item in a WPF ComboBox than for the items in the dropdown part?
Simple swapping between two templates if the item is selected or not.
I have a nested bunch of objects in an Observable Collection which is bound to a DataGrid. A IMultiValueConverter is used to gather information from two properties; this works when I do so in a DataGridTextColumn, but fails in a DataGridTemplateColumn. It's a complex situation, so I'll break it down further and post a simplified version of my code.
The nesting of the each list item is as follows:
User_Ext class which inherits the User class which has a property of the User_Rank class which in turn has a property of the User class. Unfortunately, this nesting is necessary for the way the program is set up.
There is also a separate list of Rank objects which is bound as the options for a ComboBox in a DataGridTemplateColumn, which will switch the Rank from the item in the ObservableCollection.
The Rank has a boolean property Require_License and the User has a string property License. The idea is the highlight the cell, using the IMultiValueConverter, if the License is blank and the Require_License is true.
I've included both a DataGridTextColumn and a DataGridTemplateColumn in my example code here to more easily demonstrate what is happening.
For the DataGridTextColumn bound to License, the converter fires as soon as I edit the content of either the Rank cell's ComboBox choice or the License text, and all the information carries over.
For the DataGridTemplateColumn bound to License, the converter only fires when I change the ComboBox choice, but not when I edit the License text. On top of that, when the converter catches the ComboBox change, the value for license is an empty string (not an UnsetValue) rather than the cell's content, while the second bound value (the Rank choice) is correct. I should also mention here that any changes being made are correctly updating the items in the ObservableCollection, so that aspect of the binding is working properly.
I've gotten as far as I have with my searches here, but I can't seem to find the solution to this problem.
I apologize in advance if anything is messy or forgotten, but I had to strip down my work's identifying markers and wanted to include as much as possible, since I'm not sure where the issue is occurring. My code is operational, however, if it helps to copy it into a project and test it out. I also apologize if I've been too verbose; this is my first question here and I'm not sure how much wording is appropriate to describe this situation.
As for why I don't just use the functional DataGridTextColumn, there are more things I need to put into place for which I will require the flexibility of the DataGridTemplateColumn.
Here is my XAML:
<Window x:Class="Tool.Transfer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:Tool"
Title="MainWindow" Height="350" Width="525"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
>
<Window.Resources>
<src:MatchMultiCellColourConverter x:Key="MatchMultiCellColourConverter"/>
</Window.Resources>
<Grid>
<DataGrid ItemsSource="{Binding UserImport, Mode=TwoWay}" AutoGenerateColumns="False">
<DataGrid.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Black"/>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Header="User" Binding="{Binding User_Code}"/>
<DataGridComboBoxColumn Header="Rank" DisplayMemberPath="Desc" SelectedValuePath="Code" SelectedItemBinding="{Binding user_Rank.rank}">
<DataGridComboBoxColumn.ElementStyle>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="ItemsSource" Value="{Binding Path=TargetRanks, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"/>
<Setter Property="DisplayMemberPath" Value="Desc"/>
<Setter Property="Background" Value="White"/>
</Style>
</DataGridComboBoxColumn.ElementStyle>
<DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="ItemsSource" Value="{Binding Path=TargetRanks, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"/>
<Setter Property="DisplayMemberPath" Value="Desc"/>
</Style>
</DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>
<DataGridTextColumn Header="TextColumn License" Binding="{Binding License}">
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Style.Setters>
<Setter Property="Background">
<Setter.Value>
<MultiBinding Converter="{StaticResource MatchMultiCellColourConverter}">
<Binding Path="License"/>
<Binding Path="user_Rank.rank"/>
</MultiBinding>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTemplateColumn Header="TemplateColumn License">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding License, UpdateSourceTrigger=PropertyChanged}">
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Setters>
<Setter Property="Background">
<Setter.Value>
<MultiBinding Converter="{StaticResource MatchMultiCellColourConverter}">
<Binding Path="License"/>
<Binding Path="user_Rank.rank"/>
</MultiBinding>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
</TextBox.Style>
</TextBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
And my C#:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace Tool
{
public partial class Transfer
{
private ObservableCollection<User_Ext> _userImport = null;
public ObservableCollection<User_Ext> UserImport
{
get
{
if (_userImport == null)
{
_userImport = new ObservableCollection<User_Ext>();
}
return _userImport;
}
set { _userImport = value; }
}
private ObservableCollection<Rank> _targetRanks = null;
public ObservableCollection<Rank> TargetRanks
{
get
{
if (_targetRanks == null)
{
_targetRanks = new ObservableCollection<Rank>();
}
return _targetRanks;
}
set { _targetRanks = value; }
}
public Transfer()
{
Rank r1 = new Rank(); r1.Code = "R1"; r1.Desc = "Rank1"; r1.Require_License = false;
Rank r2 = new Rank(); r2.Code = "R2"; r2.Desc = "Rank2"; r2.Require_License = true;
User a = new User(); a.User_Code = "A"; a.License = ""; a.user_Rank = new User_Rank(); a.user_Rank.rank = r1;
User b = new User(); b.User_Code = "B"; b.License = ""; b.user_Rank = new User_Rank(); b.user_Rank.rank = r2;
TargetRanks.Add(r1); TargetRanks.Add(r2);
UserImport.Add(new User_Ext(a)); UserImport.Add(new User_Ext(b));
InitializeComponent();
}
}
public class MatchMultiCellColourConverter : IMultiValueConverter
{
#region IValueConverter Members
public object Convert(object[] value, Type targetRank, object parameter, System.Globalization.CultureInfo culture)
{
if (targetRank != typeof(Brush))
throw new InvalidOperationException("The target must be a Brush");
bool pass = false;
if ( value[0] != DependencyProperty.UnsetValue && value[1] != DependencyProperty.UnsetValue)
{
String l = (String)value[0];
Rank r = (Rank)value[1];
pass = !((l ?? "") == "" && r.Require_License);
}
return pass ? Brushes.White : Brushes.Pink;
}
public object[] ConvertBack(object value, Type[] targetRank, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
#endregion
}
public class User_Ext : User, INotifyPropertyChanged
{
private bool _isComplete;
public bool IsComplete
{
get { return _isComplete; }
set
{
_isComplete = value;
NotifyPropertyChanged("IsComplete");
}
}
public User_Ext(User u) : base(u)
{
IsComplete = false;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
public class User
{
public string User_Code { get; set; }
public string License { get; set; }
public User_Rank user_Rank { get; set; }
public User() { }
public User(User u)
{
User_Code = u.User_Code;
License = u.License;
user_Rank = u.user_Rank;
}
}
public class User_Rank
{
public Rank rank { get; set; }
}
public class Rank
{
public string Code { get; set; }
public string Desc { get; set; }
public bool Require_License { get; set; }
}
}
EDIT 2017-07-25
I've been playing around with it more and I've found that I have the same problem with the DataGridCheckboxColumn. Now, I don't know much about the inner functioning of controls, but this is what I've observed.
-The IMultiValueConverter does indeed see the initial value of the cell.
-When using a DataGridTemplateColumn with a TextBox within, the ItemsSource bound to the DataGrid, UserImport, does get updated when changes are made in the DataGrid. However, the IMultiValueConverter doesn't fire when the bound License changes. It does when the bound user_Rank.rank changes (in a DataGridComboBoxColumn), but even so, the License changes are not reflected.
-If I try to use a DataGridCheckBoxColumn, the same is true.
-If I click a column header, causing a Sort Columns to occur, the IMultiValueConverter will pick up the value of License at the time of the sort, but no updates afterwards.
-If I use a DataGridTemplateColumn with a DatePicker within, I have the same problem as the other cases: the IMultiValueConverter doesn't pick up changes... except for when it does. If I click about madly, inside the text area, on the date picker button, choosing a date in the date picker, clicking on the little space to the right of the date picker button, and clicking away from the box, I've found that sometimes the IMultiValueConverter will fire. Sometimes it's when I click a date in the DatePicker, sometimes it's when I click that space next to the button, sometimes it's when I click on another cell after having clicked on that space next to the button.
So, I have a value updated in the cell, updated in the bound object, yet somehow not being picked up by the IMultiValueConverter, except for under certain circumstances. It's as though there is a third spot the data is being stored. I wonder (again, no inner knowledge of the controls) whether some cell content updates only the Control within the cell but not the cell itself when you click away.
Is it possible that the control within a cell may have it's "Value" measured separately from the cell, until it "Updates" the cell's "Value"? If that were the case, and the controls are updating the bound object without updating the cell, and the IMultiValueConverter is looking at the cell but not the bound object or the Control within the cell... maybe that would be my problem?
Please, someone, tell me how wrong I am, followed by an explanation for this phenomenon. :)
EDIT
I've found a solution which I will post.
It's because your License property doesn't raise the PropertyChanged event when it is changed.
Change this:
public class User
{
public string User_Code { get; set; }
public string License { get; set; }
public User_Rank user_Rank { get; set; }
public User() { }
public User(User u)
{
User_Code = u.User_Code;
License = u.License;
user_Rank = u.user_Rank;
}
}
To this:
public class User : INotifyPropertyChanged
{
public string User_Code { get; set; }
string _license;
public string License
{
get { return _license; };
set
{
_license = value;
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs("License"));
}
}
public User_Rank user_Rank { get; set; }
public User() { }
public event PropertyChangedEventHandler PropertyChanged;
public User(User u)
{
User_Code = u.User_Code;
License = u.License;
user_Rank = u.user_Rank;
}
}
And then cleanup the re-implementation of INotifyPropertyChanged in the derived classes.
I've found a solution.
Although I'm not sure why it was able to properly find user_Rank.rank and not License, since they are bound to the same object, it seems it was getting lost trying to find License.
If I had it look at it's own content instead, which is bound to the object anyhow, it could carry it properly to the IMultiValueConverter.
I changed the DataGridTemplateColumn code slightly to do that:
<DataGridTemplateColumn Header="TemplateColumn License">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding License}">
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Setters>
<Setter Property="Background">
<Setter.Value>
<MultiBinding Converter="{StaticResource MatchMultiCellColourConverter}">
<Binding Path="Text" RelativeSource="{RelativeSource Self}"/>
<Binding Path="user_Rank.rank"/>
</MultiBinding>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
</TextBox.Style>
</TextBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
Now IMultiValueConverter picks up each change immediately.
This can be applied to the other examples I provided. For a DataGridCheckBoxColum, the TargetType I used for the style was DataGridCell, so I used Path="Content.IsChecked" to access the CheckBox.
I haven't exactly solved the mystery, but I've come up with something so that I can move forward with my program. If anyone has the smarter answer, please feel free to lay it out for us. :)
I'm having a TextBox, if the TextBox has the Text.Length >0 then I have to change the HasChar property True otherwise False. Here I can't able to Bind the Property in the Setter.
The XAML Source Code:
<TextBox Text="WPF">
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger Value="0"
Binding="{Binding Text.Length, RelativeSource={RelativeSource Self}}">
<Setter Property="{Binding HasChar}" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
The View Model C# Source Code :
private bool _hasChar= true;
public bool HasChar
{
get { return _hasChar; }
set
{
_hasChar= value;
OnPropertyChanged();
}
}
You're misusing triggers.
The right way to go:
1) XAML:
<TextBox Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"/>
2) view model. You don't need to add setter to HasChar. If this property is bound to something in view, just raise appropriate PropertyChanged:
public class ViewModel : INotifyPropertyChanged
{
// INPC implementation is omitted
public string Text
{
get { return text; }
set
{
if (text != value)
{
text = value;
OnPropertyChanged();
OnPropertyChanged("HasChar");
}
}
}
private string text;
public bool HasChar
{
get { return !string.IsNullOrEmpty(Text); }
}
}
You can not bind a property in the setter. Style is used to set the UI element properties like Text,Visibility,Foreground etc.
I have a WPF DataGridCheckBoxColumn, which is bound to an object that implements INotifyPropertyChanged as shown below:
DataGridCheckBoxColumn Binding="{Binding Path=IsSelected}" CellStyle="{StaticResource MyDataGridCheckBoxCellStyle}"/>
Here is the associated object:
public class ListItem : INotifyPropertyChanged
{
public int ID { get; set; }
private bool isSelected = false;
public bool IsSelected { get { return isSelected; } set { isSelected = value; OnChanged("IsSelected"); } }
public event PropertyChangedEventHandler PropertyChanged;
private void OnChanged(string prop)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
Everything works as expected, except that in order to set the Checkbox to Checked, I need to double click, in order to first select the column, and then set the checkbox value.
So, I decide to implement a Style trigger as shown below:
<Style x:Key="MyDataGridCheckBoxCellStyle" TargetType="DataGridCell">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True" >
<Setter Property="IsEditing" Value="True" />
</Trigger>
</Style.Triggers>
</Style>
Now I am able to single click to Check the Checkbox, but my binding doesn't work anymore. Any idea of what is going on here? Why does setting the Style Trigger remove the binding?
Changing the style can cause some issues with the default template. You would most likely need to copy the entire style + template and then modify that to suit your needs. You could try this http://wpf.codeplex.com/wikipage?title=Single-Click%20Editing however.
View Model
public class MyViewModel
{
public string MyProperty { get; set; }
}
XAML
<CheckBox IsChecked="{Binding !MyProperty.Equals('Steve')}" />
Is this possible? How?
This sort of thing can be done (and many say should be done) in Xaml without involving logic from the View Model. To see it work, create a View Model like this...
public class ViewModel : INotifyPropertyChanged
{
private string _myProperty;
public string MyProperty
{
[DebuggerStepThrough]
get { return _myProperty; }
[DebuggerStepThrough]
set
{
if (value != _myProperty)
{
_myProperty = value;
OnPropertyChanged("MyProperty");
}
}
}
#region INotifyPropertyChanged Implementation
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string name)
{
var handler = System.Threading.Interlocked.CompareExchange(ref PropertyChanged, null, null);
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
#endregion
}
And then bind it to some Xaml that looks like this...
<Grid>
<CheckBox Content="Some check box">
<CheckBox.Style>
<Style TargetType="CheckBox">
<Setter Property="IsChecked" Value="False"/>
<Style.Triggers>
<DataTrigger Binding="{Binding MyProperty}" Value="Steve">
<Setter Property="IsChecked" Value="True"/>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
</CheckBox>
</Grid>
This is a standard WPF Checkbox that has been styled with a data trigger. The trigger will set the IsChecked property to true whenever the 'MyProperty' property contains "Steve". Otherwise the CB will be unchecked (per the overriding Setter in the Style). It works because the trigger listens to changes in the VM's 'MyProperty'. So visualization is entirely relegated to the user surface.
Triggers can be combined (and even used with Template Selectors) to access powerful functions built-in to WPF; and they will bind to any dependency property on the Check box, like Background colour etc.
A lot of people will suggest a converter, which certainly works. But I've found a much quicker way is to create a new bool property to use and bind to that:
public string MyProperty{get;set;}
public bool MyPropertyChecked
{
get { return !MyProperty.Equals('Steve')}
}