I'm having an issue causing a label to refresh when it's content doesn't change but its format does. The ContentStringFormat property is bound to the viewmodel and the property change is notified, but the label doesn't update, please find bellow a minimal reproduction exemple in code as well as a project ready to compile/run that demonstrates the issue.
Download project : https://www.dropbox.com/s/rjs1lot09uc2lgj/WPFFormatBindingRefresh.zip?dl=0
XAML :
<StackPanel>
<Label Content="{Binding FirstLabelContent}"></Label>
<Label Content="{Binding SecondLabelContent}" ContentStringFormat="{Binding SecondLabelFormatContent}"></Label>
<Button Click="Button_Click">Add "test" to all bound elements</Button>
</StackPanel>
Code behind :
public event PropertyChangedEventHandler PropertyChanged = (a,b)=> { }; // empty handler avoids checking for null when raising
public string FirstLabelContent { get; set; } = "First Label";
public string SecondLabelContent { get; set; } = "Second";
public string SecondLabelFormatContent { get; set; } = "{0} Label";
void PropertyChange(string PropertyName)
{
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
private void Button_Click(object sender, RoutedEventArgs e)
{
FirstLabelContent += " TEST";
SecondLabelFormatContent += " TEST";
PropertyChange("FirstLabelContent"); // First label correctly updates
PropertyChange("SecondLabelFormatContent"); // Second label doesn't update, expected behavior is changing the format string should cause an update
}
The Label doesn't support refreshing the ContentStringFormat through a binding.
You could use a multi converter like this:
public class MultiConverter2 : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
string SecondLabelContent = values[0] as string;
string SecondLabelFormatContent = values[1] as string;
return string.Format(SecondLabelFormatContent, SecondLabelContent);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
XAML:
<StackPanel>
<StackPanel.Resources>
<local:MultiConverter2 x:Key="conv" />
</StackPanel.Resources>
<Label Content="{Binding FirstLabelContent}"></Label>
<Label>
<Label.Content>
<MultiBinding Converter="{StaticResource conv}">
<Binding Path="SecondLabelContent" />
<Binding Path="SecondLabelFormatContent" />
</MultiBinding>
</Label.Content>
</Label>
<Button Click="Button_Click">Add "test" to all bound elements</Button>
</StackPanel>
Related
Edit: I created a sample project displaying what I have done and what doesn't work. https://github.com/jmooney5115/clear-multibinding
I have a WPF application with controls (textbox, datagrid, etc). When a value changes on the control I need to indicate it by changing the background color. After saving changes the background color needs to go back to the unchanged state without reloading the control. This application is not MVVM, don't judge I inherited it.
I have the code working perfectly for changing the color using MultiBinding and a value converter. The problem is I cannot figure out how to reset the background after calling Save() in my code. I have tried doing DataContext = null and then DataContext = this but the control flickers. There has to be a better way.
Q: how can I reset the background to the unchanged state without reloading the control?
MultiBinding XAML - this works by passing a string[] to BackgroundColorConverter. string[0] is the OneTime binding. string1 is the other binding.
<TextBox.Background>
<MultiBinding Converter="{StaticResource BackgroundColorConverter}">
<Binding Path="DeviceObj.Name" />
<Binding Path="DeviceObj.Name" Mode="OneTime" />
</MultiBinding>
</TextBox.Background>
BackgroundColorConverter.cs
/// <summary>
/// https://stackoverflow.com/questions/1224144/change-background-color-for-wpf-textbox-in-changed-state
///
/// Property changed
/// </summary>
public class BackgroundColorConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var colorRed = (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("#FFB0E0E6");
var colorWhite = (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString("White");
var unchanged = new SolidColorBrush(colorWhite);
var changed = new SolidColorBrush(colorRed);
if (values.Length == 2)
if (values[0].Equals(values[1]))
return unchanged;
else
return changed;
else
return changed;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Updates
Edit: this is the multi binding for a data grid cell. If the multi binding converter returns true, set the background color to LightBlue. If false, the background is the default color.
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Path=Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" >
<!-- https://stackoverflow.com/questions/5902351/issue-while-mixing-multibinding-converter-and-trigger-in-style -->
<DataGridTextColumn.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource BackgroundColorConverterBool}">
<Binding Path="Name" />
<Binding Path="Name" Mode="OneTime" />
</MultiBinding>
</DataTrigger.Binding>
</DataTrigger>
<Setter Property="Background" Value="LightBlue"></Setter>
</Style.Triggers>
</Style>
</DataGridTextColumn.CellStyle>
</DataGridTextColumn>
.
.
.
</DataGrid.Columns>
I made this method to reset the binding of objects after saving.
/// <summary>
/// Update the data binding after a save to clear the blue that could be there when
/// a change is detected.
/// </summary>
/// <typeparam name="T">Type to search for</typeparam>
/// <param name="parentDepObj">Parent object we want to reset the binding for their children.</param>
public static void UpdateDataBinding<T>(DependencyObject parentDepObj) where T : DependencyObject
{
if (parentDepObj != null)
{
MultiBindingExpression multiBindingExpression;
foreach (var control in UIHelper.FindVisualChildren<T>(parentDepObj))
{
multiBindingExpression = BindingOperations.GetMultiBindingExpression(control, Control.BackgroundProperty);
if (multiBindingExpression != null)
multiBindingExpression.UpdateTarget();
}
}
}
Final Update
This question answers how to use MultiBinding for my purpose on DataGridCell: Update MultiBinding on DataGridCell
IHMO a MVVM solution (as Rekshino proposed) is for sure better than a not-MVVM one. The view model should take care about tracing modified data.
Anyway since you inherited this application, you have to consider how much time you need for converting the whole code and sometimes it is not possible. So in this case you can force every single multibinding to "refresh" when you save your data.
Let's suppose this is your XAML (with two or more TextBoxes):
<StackPanel>
<TextBox Margin="5" Text="{Binding DeviceObj.Name, Mode=TwoWay}">
<TextBox.Background>
<MultiBinding Converter="{StaticResource BackgroundColorConverter}">
<Binding Path="DeviceObj.Name" />
<Binding Path="DeviceObj.Name" Mode="OneTime" />
</MultiBinding>
</TextBox.Background>
</TextBox>
<TextBox Margin="5" Text="{Binding DeviceObj.Surname, Mode=TwoWay}">
<TextBox.Background>
<MultiBinding Converter="{StaticResource BackgroundColorConverter}">
<Binding Path="DeviceObj.Surname" />
<Binding Path="DeviceObj.Surname" Mode="OneTime" />
</MultiBinding>
</TextBox.Background>
</TextBox>
<Button Content="Save" Click="Button_Click" Margin="5,10,5,10" />
</StackPanel>
When you click the "Save" Button you can force MultiBindings to update their own targets in this way:
private void Button_Click(object sender, RoutedEventArgs e)
{
MultiBindingExpression multiBindingExpression;
foreach (TextBox textBox in FindVisualChildren<TextBox>(this))
{
multiBindingExpression = BindingOperations.GetMultiBindingExpression(textBox, TextBox.BackgroundProperty);
multiBindingExpression.UpdateTarget();
}
}
You can find the FindVisualChildren implementation in this answer. I hope it can help you.
You have to paste a bool Saved property to your DeviceObj and handle it, if Name or something else been changed.
ViewModel:
public class Device : INotifyPropertyChanged
{
public string Name
{
get
{
return _name;
}
set
{
if (value != _name)
{
_name = value;
Saved = false;
NotifyPropertyChanged(nameof(Name));
}
}
}
private string _name;
public bool Saved
{
get
{
return _saved;
}
set
{
if (value != _saved)
{
_saved = value;
NotifyPropertyChanged(nameof(Saved));
}
}
}
private bool _saved = true;
public void Save()
{
//Saving..
Saved = true;
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string info)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(info));
}
}
Converter:
public class BoolToSolColBrushConverter : IValueConverter
{
private static SolidColorBrush changedBr = new SolidColorBrush(Colors.Red);
private static SolidColorBrush unchangedBr = new SolidColorBrush(Colors.Green);
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
try
{
if ((bool)value)
{
return unchangedBr;
}
}
catch (Exception)
{
}
return changedBr;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
XAML:
<TextBox Text="{Binding Name}" Background="{Binding Saved, Converter={StaticResiurce BoolToSolColBrushConverter}}" />
I have simple button which do specific action like edit element. And I would like to pass a few parameters including object specifically SelectedMatch from datagrid and SelectedDate from callendar. Now i have something like this :
<DatePicker x:Name="dpEditDateMatch" SelectedDateFormat="Long" SelectedDate="{Binding MyDateTimeProperty2, Mode=TwoWay}" ></DatePicker>
<Button Content="Edit match" Command="{Binding EditMatchCommand}">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource Converter}">
<Binding >??</Binding>
<Binding Path="SelectedDate" ElementName="dpEditDateMatch"/>
</MultiBinding>
</Button.CommandParameter>
</Button>
<ScrollViewer>
<DataGrid IsReadOnly="True" ItemsSource="{Binding match}" SelectedItem="{Binding SelectedMatch}"/>
</ScrollViewer>
In .cs file i have definition of SelectedMatch like this:
object _SelectedMatch;
public object SelectedMatch
{
get
{
return _SelectedMatch;
}
set
{
if (_SelectedMatch != value)
{
_SelectedMatch = value;
RaisePropertyChanged("SelectedMatch");
}
}
}
How can i do that ? I mean, handle this from xaml.
I just faced the problem you said yesterday.
And I solved it now.
Here is the XAML:
<Page.Resources>
<local:MultiIcommandParameterConverter x:Key="MultiIcommandParameterConverter"></local:MultiIcommandParameterConverter>
</Page.Resources>
<ItemsControl>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel></WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Command="{Binding ButtonClick}">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource MultiIcommandParameterConverter}">
<Binding Path="DetailIndex"/>
<Binding Path="DetailContent"/>
</MultiBinding>
</Button.CommandParameter>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
And here is code behind of INotifyPropertyChanged:
public class DetailButtonClass: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChange(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
string _DetailContent;
public string DetailContent
{
get
{
return _DetailContent;
}
set
{
_DetailContent = value;
NotifyPropertyChange("DetailContent");
}
}
string _DetailIndex;
public string DetailIndex
{
get
{
return _DetailIndex;
}
set
{
_DetailIndex = value;
NotifyPropertyChange("DetailIndex");
}
}
CommandClass _ButtonClick;
public override CommandClass ButtonClick
{
get
{
return _ButtonClick;
}
set
{
_ButtonClick = value;
NotifyPropertyChange("ButtonClick");
}
}
public class CommandClass : ICommand
{
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public delegate void DelegateClickVoid(string index,string content);
public DelegateClickVoid ClickVoid = null;
public void Execute(object parameter)
{
string[] parameterArray = parameter.ToString().Split(",".ToArray());
ClickVoid?.Invoke(parameterArray[0], parameterArray[1]);
}
}
}
And here is code behind about IMultiValueConverter,the most important things:
public class MultiIcommandParameterConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values[0] + "," + values[1];
}
object[] IMultiValueConverter.ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Totally,convert all the parameters into one parameter by using ',' to split it.While the button clicked,split the parameter by ',' into parameters.
You already have the data in your ViewModel i.e. MyDateTimeProperty2 which is for your date and SelectedMatch for the match. In your MultiBinding you should pass this:
<Button Content="Edit match" Command="{Binding EditMatchCommand}">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource Converter}">
<Binding Path="MyDateTimeProperty2" />
<Binding Path="SelectedMatch" />
</MultiBinding>
</Button.CommandParameter>
</Button>
Because Button already has the same DataContext as the DatePicker and DataGrid you don't even need that x:Name="dpEditDateMatch" on the DatePicker to get the data.
I effectively have -
<UserControl ...>
<Grid>
<TreeView Name="nTree">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="NodeType" ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<Image x:Name="icon" />
<TextBlock Text="{Binding Name}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
</UserControl>
NodeType is a type returned by an underlying library. I would like to set the icon images source based on the name provided - however I can't modify NodeType to add a getter.
So ideally what I would like is the icon image source to be bound to a function on the UserControl class which receives the Name and returns an ImageSource.
i.e.
public partial class Panel : UserControl
{
public Panel(NodeType n)
{
nTree.Items.add(n);
}
public ImageSource GetIcon(string name)
{
...
}
}
This feels like it should be possible but i'm struggling to work it out. Assistance would be appreciated.
Unfortunately you can't bind to methods, you need to convert the method to a property
you can do this in several ways
the easiest would be to have: (though this should be on your VM not your V)
public ImageSource Icon
{
...
}
or you can use a value converter:(the best fit for what you are descibing)
public class ImageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
GetImageLogic
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException("One way only");
}
}
the 3rd option would be to use an CommandBinding :
then bind to the command
CommandBinding OpenCmdBinding = new CommandBinding(
ApplicationCommands.Open,
OpenCmdExecuted,
OpenCmdCanExecute);
this.CommandBindings.Add(OpenCmdBinding);
void OpenCmdExecuted(object target, ExecutedRoutedEventArgs e)
{
String command, targetobj;
command = ((RoutedCommand)e.Command).Name;
targetobj = ((FrameworkElement)target).Name;
MessageBox.Show("The " + command + " command has been invoked on target object " + targetobj);
}
void OpenCmdCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
In the following DataTemplate, the first binding doesn't work while the 2nd one works, and I would like to know why.
<local:IsEnabledConverter x:Key="isEnabled"/>
<local:Boolean2TextConverter x:Key="txtConverter"/>
<DataTemplate x:Key="fileinfoTemplate" DataType="{x:Type local:MyFileInfo}">
<StackPanel>
<Label x:Name="1stLabel" Content="{Binding Path=Filename}" IsEnabled="{Binding Path=., Converter={StaticResource isEnabled}}"/> <--- doesn't work
<Label x:Name="2ndLabel" Content="{Binding Path=IfPrint, Converter={StaticResource txtConverter}}" IsEnabled="{Binding Path=IsChecked, ElementName=ckBox}"/> <--- works
<CheckBox x:Name="ckBox" IsChecked="{Binding Path=IfPrint}" IsEnabled="{Binding Path=IsValid}" Style="{StaticResource printCkBox}"/>
</StackPanel>
</DataTemplate>
IsEnabledConverter:
class IsEnabledConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
MyFileInfo f = value as MyFileInfo;
return f.IsValid && f.IfPrint;
}
//... omit ConvertBack NotImplementedException stuff
}
Boolean2TextConverter:
class IsEnabledConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
Boolean b = (Boolean)value;
return b.ToString();
}
//similarly omit ConvertBack here
}
Code for MyFileInfo:
public class MyFileInfo {
public string IfPrint {
get;
set;
}
public string IsValid {
get;
set;
}
...
}
Problem: When the CheckBox is toggled, the 2nd Label grays out and shows "false", or becomes normal and shows "true", as it should. However, the first Label doesn't change at all; its IsEnabled state is supposed be the conjunction of two Booleans, one of which is changed by the CheckBox. What is wrong? (note that the IsEnabledConverter is called once upon GUI initialization, but not called again when its binding source changes.)
There are 2 issues here. First you have to implement INotifyPropertyChanged for the ViewModel MyFileInfo. Secondly you have to use MultiBinding here. Because I don't think we have some way to trigger updating the target (such as when toggling the CheckBox) if you bind the whole view model to the IsEnabled target. So here is how it should be done:
Your view model:
public class MyFileInfo : INotifyPropertyChanged {
bool _ifPrint;
bool _isValid;
public bool IfPrint {
get { return _ifPrint; }
set {
if(_ifPrint != value) {
_ifPrint = value;
OnPropertyChanged("IfPrint");
}
}
}
public bool IsValid {
get { return _isValid; }
set {
if(_isValid != value) {
_isValid = value;
OnPropertyChanged("IsValid");
}
}
}
//Implement INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string prop){
var handler = PropertyChanged;
if(handler != null) handler(this, new PropertyChangedEventArgs(prop));
}
//.... should do the same for the remaining properties....
//...
}
Here is the converter used for MultiBinding, which should implement IMultiValueConverter (instead of IValueConverter):
class IsEnabledConverter : IMultiValueConverter {
public object Convert(object[] values, Type targetType, object parameter,
System.Globalization.CultureInfo culture) {
if(values.Length == 2){
return (bool) values[0] && (bool) values[1];
}
return false;
}
//... omit ConvertBack NotImplementedException stuff
}
Here is the modifed XAML (to use MultiBinding instead):
<Label x:Name="firstLabel" Content="{Binding Path=Filename}">
<Label.IsEnabled>
<MultiBinding Converter="{StaticResource isEnabled}">
<Binding Path="IsValid"/>
<Binding Path="IfPrint"/>
</MultiBinding>
</Label.IsEnabled>
</Label>
Now one of IsValid and IfPrint changing will trigger the MultiBinding's Converter. Here you can also bind to IsChecked of the CheckBox directly instead of indirectly via IfPrint.
PS: Note Name used in XAML (as well as in codebehind) must not start with number.
Since the instance of MyFileInfo does not change while you check/uncheck the checkbox hence IsEnabledConverteris not getting called.
In order to Enable/Disable your 1stLabel depending on two properties, either use MultiValueConverter or use MultiDataTrigger by applying Style to your Label.
I have a Listbox bound to an ObservableCollection of ImageMetadata class. Item template of Listbox is defined as
<Image Source="{Binding Converter={StaticResource ImageConverter}}" />
ImageConverter is written as
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var metadata = (ImageMetadata)value;
if (metadata.IsPublic)
{
//code to return the image from path
}
else
{
//return default image
}
}
ImageMetadata is the 'Model' class written as
class ImageMetadata : INotifyPropertyChanged
{
public string ImagePath
{
......
}
public bool IsPublic
{
......
}
}
When an image is updated, I will trigger PropertyChanged event as given below
NotifyPropertyChanged("ImagePath");
Problem here is that : NotifyPropertyChanged event will not work since I am specifying the changed property name as 'ImagePath' and the binding is to 'ImageMetadata' object rather than 'ImagePath' property.
I cannot use
<Image Source="{Binding ImagePath, Converter={StaticResource ImageConverter}}" />
since I need the IsPublic property also to decide which image to display.
How can I modify the code to properly fire PropertyChanged event?
Edit : I am developing for Windows phone 8.
You could use a MultiBinding with a multi-value converter:
<Image>
<Image.Source>
<MultiBinding Converter="{StaticResource ImageConverter}">
<Binding Path="ImagePath"/>
<Binding Path="IsPublic"/>
</MultiBinding>
</Image.Source>
</Image>
The Convert method would look like this:
public object Convert(
object[] values, Type targetType, object parameter,CultureInfo culture)
{
object result = null;
if (values.Length == 2 && values[0] is string && values[1] is bool)
{
var imagePath = (string)values[0];
var isPublic = (bool)values[1];
...
}
return result;
}