I want to compare two versions of various properties and bold one of them if it is not equal to the other. Since SL4 doesn't support MultiBinding I am binding the FontWeight to "." so that the entire data context is passed to the converter. I then use the converter parameter to specify which fields to compare within the converter. So far, so good... Values that don't match are bolded.
The problem is that the bolded property is bound to a text box which can be edited. When the value is edited, I want the converter to be "re-activated" so that the font weight is set according to the new value. This doesn't happen. How can this be accomplished?
Note: I have already implemented INotifyPropertyChanged for the relevant class and properties. Tabbing to the next field after changing the value causes the PropertyChanged event to fire, but the font weight is not updated until I actually move to a different record and then return to the record that was changed.
(I also tried using Mode=TwoWay to see if that would do the trick. However, TwoWay binding cannot be used when you are binding to ".")
Do you NEED to use a Value Converter? I tried this quick using the MVVM pattern and it worked pretty well. If you could use MVVM, you could possibly do it like this:
MainPage.xaml
<UserControl x:Class="BindBoldText.MainPage"
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:BindBoldText"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.DataContext>
<local:MainPage_ViewModel/>
</UserControl.DataContext>
<StackPanel>
<TextBlock Text="{Binding Value1, Mode=TwoWay}"/>
<TextBlock Text="{Binding Value2, Mode=TwoWay}" FontWeight="{Binding Value2FontWeight}"/>
<TextBox Text="{Binding Value2, Mode=TwoWay}" TextChanged="TextBox_TextChanged"/>
</StackPanel>
MainPage.xaml.cs
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
this.viewModel = this.DataContext as MainPage_ViewModel;
}
private MainPage_ViewModel viewModel;
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
viewModel.Value2 = (sender as TextBox).Text;
}
}
MainPage_ViewModel.cs
public class MainPage_ViewModel : INotifyPropertyChanged
{
public string Value1
{
get { return value1; }
set
{
if (value1 != value)
{
value1 = value;
OnPropertyChanged("Value1");
}
}
}
private string value1 = "Test";
public string Value2
{
get { return value2; }
set
{
if (value2 != value)
{
value2 = value;
OnPropertyChanged("Value2");
OnPropertyChanged("Value2FontWeight");
}
}
}
private string value2 = "Test";
public FontWeight Value2FontWeight
{
get
{
if (value2.Equals(value1))
{
return FontWeights.Normal;
}
else
{
return FontWeights.Bold;
}
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
In your solution, the value does not get updated because the property itself is bound against the whole data context via the "." expression. INotifyPropertyChanged may be called, but this event means that a single property has changed, and since you don't provide a property name in the binding expression, the data binding system does not know that the Binding needs to be updated - it can't look into what your value converter does.
I think JSprang's appraoch is a lot better, not at least because it provides better separation of presentation logic that can be tested from the markup. To go further with a clean interface, you could let the ViewModel implement a boolean property "ValuesAreSame", data-bind against it, and use a value converter to apply the actual visual style (in this case, a font weight).
Related
I'm trying to make a control that has a current value with an optional equation string.
I have 2 textboxes:
One (a) where you can enter an equation shortcut to a value to put into the other (b).
(b) contains the actual value.
(for example, in (a), if you enter 'pi', the second will then fill with "3.1415926535897931")
I'm using 2 textboxes so the user can refine their equation if they need to, and watch the value change as they modify it.
The data has 2 fields, one being the equation string and the other being the current value.
so I have (a).Text bound to the string, a new property on (a) that holds the value, and I bind (b).Text to the value also.
(a).Text is TwoWay
(a).Value is OneWayToSource (since changes to the text should only be pushed to b)
(b).Value is TwoWay
This all works fine if I have the data set in the constructor before any XAML binding, but does not work at all if I add the data after binding.
Here is a minimal amount of code that shows the problem.
The only comment is at the line that can make it work or not.
As a last resort I could turn it into a custom control and handle it in the code-behind, but I'd think this should work in the first place.
Any ideas why this isn't working?
Thanks!
Here is the XAML:
<Window x:Class="twoBindingsOnSameField.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:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:twoBindingsOnSameField"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<Button Content="load data" Click="Button_Click" Width="80" IsEnabled="{Binding NeedsData}"/>
<StackPanel Orientation="Horizontal">
<TextBlock Text="enter text:" Width="80"/>
<local:TextBoxCalc Text="{Binding Item.ItemString, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
TextBoxCalculatedValue="{Binding Item.ItemValue, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}"
Width="200"
IsEnabled="{Binding HasData}"
/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="updated text:" Width="80"/>
<TextBox Text="{Binding Item.ItemValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Width="200"
IsEnabled="{Binding HasData}"
/>
</StackPanel>
</StackPanel>
</Window>
Here is the codebehind.
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
namespace twoBindingsOnSameField
{
public partial class MainWindow : Window
{
data data;
public MainWindow()
{
InitializeComponent();
data = new data();
/// ---- Does not work with the following line commented out, but does if it is uncommented ----
/// ---- use the button to set the data ----
//setdata();
DataContext = data;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
setdata();
}
void setdata()
{
if (data.Item == null)
data.Item = new dataitem();
}
}
public class data : notifybase
{
dataitem item;
public data()
{
}
public dataitem Item
{
get
{
return item;
}
set
{
if (item != value)
{
item = value;
notifyPropertyChanged("Item");
notifyPropertyChanged("HasData");
notifyPropertyChanged("NeedsData");
}
}
}
public bool HasData
{
get
{
return Item != null;
}
}
public bool NeedsData
{
get
{
return Item == null;
}
}
}
public class dataitem : notifybase
{
string itemString;
string itemValue;
public dataitem()
{
itemString = "3";
itemValue = "4";
}
public virtual string ItemString
{
get
{
return this.itemString;
}
set
{
if (!object.Equals(this.itemString, value))
{
this.itemString = value;
notifyPropertyChanged("ItemString");
}
}
}
public virtual string ItemValue
{
get
{
return this.itemValue;
}
set
{
if (!object.Equals(this.itemValue, value))
{
this.itemValue = value;
notifyPropertyChanged("ItemValue");
}
}
}
}
public class TextBoxCalc : TextBox
{
public TextBoxCalc()
{
TextProperty.AddHandler(this, (o,e)=>TextBoxCalculatedValue="updated:" + Text);
}
#region TextBoxCalculatedValue
public static DependencyProperty TextBoxCalculatedValueProperty = DependencyProperty.Register("TextBoxCalculatedValue", typeof(string), typeof(TextBoxCalc), new PropertyMetadata(""));
public string TextBoxCalculatedValue
{
get
{
return (string)GetValue(TextBoxCalculatedValueProperty);
}
set
{
if (!object.Equals(TextBoxCalculatedValue, value))
SetValue(TextBoxCalculatedValueProperty, value);
}
}
#endregion
}
public class notifybase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
protected virtual void notifyPropertyChanged(string propertyName)
{
PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
OnPropertyChanged(e);
}
}
static class extensions
{
public static void AddHandler(this DependencyProperty prop, object component, EventHandler handler)
{
DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(prop, component.GetType());
if (dpd != null)
dpd.AddValueChanged(component, handler);
}
}
}
The reason why it works when you uncomment //setdata(); is because it is initializing the object in what is effectively your viewmodel, therefore you can change its properties via binding. To clarify as a side note, data would be your view model, and dataitem is your model, however you're dataitem is using INPC, so it doesn't really make sense in this case to have a viewmodel necessarily.
Anyways, the issue is that TextBoxCalculatedValue is set to a OneWayToSource binding. When you run the code commented out, its going to try and bind to a null value. When it does, it tries to update a null value, which isn't possible. WPF handles what would normally be a null exception automatically. When you update the dataItem by clicking the button, it doesn't update the object TextBoxCalc is bound to, so instead, it will continue trying to bind & update the null object. Change it to a TwoWay binding and you'll see a difference. Changing to TwoWay is probably your best option.
Good practice is to use constructor injection to practice dependency injection. With that being said, passing a dataItem to data would be the best route, and at the very least, initializing dataItem in data's constructor would be an ideal approach. So,
public data(dataItem item)
{
Item = item;
}
or
public data()
{
Item = new dataitem();
}
I've created the simplest binding. A textbox bound to an object in the code behind.
Event though - the textbox remains empty.
The window's DataContext is set, and the binding path is present.
Can you say what's wrong?
XAML
<Window x:Class="Anecdotes.SimpleBinding"
x:Name="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="SimpleBinding" Height="300" Width="300" DataContext="MainWindow">
<Grid>
<TextBox Text="{Binding Path=BookName, ElementName=TheBook}" />
</Grid>
</Window>
Code behind
public partial class SimpleBinding : Window
{
public Book TheBook;
public SimpleBinding()
{
TheBook = new Book() { BookName = "The Mythical Man Month" };
InitializeComponent();
}
}
The book object
public class Book : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
private string bookName;
public string BookName
{
get { return bookName; }
set
{
if (bookName != value)
{
bookName = value;
OnPropertyChanged("BookName");
}
}
}
}
First of all remove DataContext="MainWindow" as this sets DataContext of a Window to a string MainWindow, then you specify ElementName for your binding which defines binding source as another control with x:Name="TheBook" which does not exist in your Window. You can make your code work by removing ElementName=TheBook from your binding and either by assigning DataContext, which is default source if none is specified, of a Window to TheBook
public SimpleBinding()
{
...
this.DataContext = TheBook;
}
or by specifying RelativeSource of your binding to the Window which exposes TheBook:
<TextBox Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}, Path=TheBook.BookName}"/>
but since you cannot bind to fields you will need to convert TheBook into property:
public partial class SimpleBinding : Window
{
public Book TheBook { get; set; }
...
}
Is there a way to call methods from the view from the view model? Is this good practice to do so? If not, how would I hide elements in the view from the view model? I'm just a bit confused because I'm used to working with ASP.Net, with code behind, etc.
xaml.cs
btnsave.visibility = visibility.hidden;
btnclose.visibility = visibility.hidden;
For your specific example of hiding elements in the view, you probably want to set up some properties in the ViewModel that define the conditions under which those elements are visible. Then you bind the Visibility property (with a BooleanToVisibilityConverter, most likely) of those elements in the View to those properties in the ViewModel.
More generally, you want to keep the direct coupling between them minimal if you can, but sometimes "reality" gets in the way. I've had some cases where I've passed in the View to the constructor of the ViewModel. Other cases where it's been an interface that the View implements and that gets passed into the ViewModel. So there are options. But you should make sure you HAVE to go that route before doing it.
Example:
XAML:
<Window ...>
<Window.Resources>
<BooleanToVisibilityConverter x:Key="_B2VC" />
</Window.Resources>
<StackPanel>
<Button Content="Save" Visibility="{Binding IsSaveButtonVisible}" />
<Button Content="Close" Visibility="{Binding IsCloseButtonVisible}" />
</StackPanel>
</Window>
ViewModel:
public class ViewModel: INotifyPropertyChanged
{
#region INPC Stuff
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
private bool _IsSaveButtonVisible;
public bool IsSaveButtonVisible
{
get { return _IsSaveButtonVisible; }
set
{
if (_IsSaveButtonVisible != value)
{
_IsSaveButtonVisible = value;
RaisePropertyChanged("IsSaveButtonVisible");
}
}
}
private bool _IsCloseButtonVisible;
public bool IsCloseButtonVisible
{
get { return _IsCloseButtonVisible; }
set
{
if (_IsCloseButtonVisible != value)
{
_IsCloseButtonVisible = value;
RaisePropertyChanged("IsCloseButtonVisible");
}
}
}
}
Then your ViewModel changes those properties in response to whatever it needs to (say for instance Save is only valid if they've changed something - once that something is changed, the property on the ViewModel gets updated and bam, that gets propogated to the View.
If you need further examples, i'd just suggest going and reading on MVVM. It takes a bit to grok, but its awesome once in use.
I'm building an application using WPF and MVVM. I've come across a situation where I have a view containing a usercontrol (representing a Timer). This usercontrol has a property in it's code behind which performs some calculations before getting and setting data.
TimerControl.xaml.cs:
public DateTime? DateTimeValue
{
get
{
string hours = this.txtHours.Text;
string minutes = this.txtMinutes.Text;
string amPm = this.txtAmPm.Text;
if (!string.IsNullOrWhiteSpace(hours) && !string.IsNullOrWhiteSpace(minutes) && !string.IsNullOrWhiteSpace(amPm))
{
string value = string.Format("{0}:{1} {2}", this.txtHours.Text, this.txtMinutes.Text, this.txtAmPm.Text);
DateTime time = DateTime.Parse(value);
return time;
}
else
{
return null;
}
}
set
{
DateTime? time = value;
if (time.HasValue)
{
string timeString = time.Value.ToShortTimeString();
//9:54 AM
string[] values = timeString.Split(':', ' ');
if (values.Length == 3)
{
this.txtHours.Text = values[0];
this.txtMinutes.Text = values[1];
this.txtAmPm.Text = values[2];
}
}
}
}
Now I wanted to bind this property to a property present in view model of the view. Following is property in the VM:
public DateTime? StartTime
{
get
{
return _StartTime;
}
set
{
_StartTime = value;
RaisePropertyChanged("StartTime");
}
}
This is how I am performing binding in the xaml of View.
MyView.xaml:
<my:TimeControl Background="White" Grid.Column="1" Grid.Row="2" Margin="3" x:Name="StartTimeControl" DateTimeValue="{Binding StartTime}" Width="150" Height="26" HorizontalAlignment="Left">
But it is giving me an error that:
A 'Binding' cannot be set on the 'DateTimeValue' property of type 'TimeControl'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
I've been struggling for hours trying to figure out a way to make this binding work. I have even tried to make a dependency property in the TimeControl's code behind for the DateTimeValue property, which has resolved the above exception, but the binding still doesn't work. Whenever I access StartTime property in the VM's code behind, it is showing null. Although it should show a valid value by getting the DateTimeValue property.
Kindly suggest me a way to make this work. Thanks.
Your implementation of DateTimeValue property shown in this question is certainly wrong and leads to exception, because DateTimeValue should be dependency property.
But you mentioned that you have tried to use dependency property with no success. I suppose the reason is in collision of DataContexts and your XAML looks like this:
<UserControl x:Class="Test.SomeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:self="clr-namespace:Test"
Name="Root">
<WrapPanel>
<self:TimerControl Time="{Binding StartTime}"/>
</WrapPanel>
</UserControl>
This code doesn't work. Why? DataContext of TimerControl is inherited (or maybe you replace it at all), meanwhile when you address to StartTime you have in mind ViewModel as DataContext. So you should clearly point to correct DataContext:
<self:Timer Time="{Binding DataContext.StartTime, ElementName=Root}"/>
===UPDATE===
The whole code of my Timer control (as you can see my Timer has textbox, when you input some text, textbox raises appropriate event, which we handle and set Time property):
public partial class Timer : UserControl
{
public Timer()
{
InitializeComponent();
}
public DateTime? Time
{
get
{
return (DateTime?)this.GetValue(Timer.TimeProperty);
}
set
{
this.SetValue(Timer.TimeProperty, value);
}
}
public static readonly DependencyProperty TimeProperty =
DependencyProperty.Register("Time",
typeof(DateTime?),
typeof(Timer),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (d, e) => { }));
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
if (DateTime.Now.Ticks % 2 == 0)
{
this.Time = DateTime.Now;
}
else
{
this.Time = null;
}
}
}
And XAML:
<UserControl x:Class="Test.Timer">
<Grid>
<TextBox TextChanged="TextBox_TextChanged"/>
</Grid>
</UserControl>
Usage of Time control in XAML:
<UserControl x:Class="Test.StartupView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:self="clr-namespace:Test"
Name="Root">
<WrapPanel>
<self:Timer Time="{Binding DataContext.StartTime, ElementName=Root}"/>
</WrapPanel>
</UserControl>
Code behind of StartupView:
public StartupView()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
Property in ViewModel remains the same. During debugging setter of StartTime property fires every time when I change text in Timer.
What excatly do you want to do?
You can't bind to a standard property. If you want to bind you should use a dependency property.
public DateTime? DateTimeValue
{
get { return (DateTime?)GetValue(DateTimeValueProperty); }
set { SetValue(DateTimeValueProperty, value); }
}
// Using a DependencyProperty as the backing store for DateTimeValue. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DateTimeValueProperty =
DependencyProperty.Register("DateTimeValue", typeof(DateTime?), typeof(TimeControl), new UIPropertyMetadata(null));
Inside the UserControl:
<TextBox Text="{Binding DateTimeValue,RelativeSource={RelativeSource AncestorLevel=1, Mode=FindAncestor,AncestorType=UserControl}, Converter=...}" />
To bind directly to a DateTimeValue is not possible because there is no converter available for string->DateTime so you have to write an IValueConverter and specify this in your binding.
From outside of course you should be able to bind the value directly.
I have a Dictionary which is binded to a combobox. I have used dictionary to provide spaces in enum.
public enum Option {Enter_Value, Select_Value};
Dictionary<Option,string> Options;
<ComboBox
x:Name="optionComboBox"
SelectionChanged="optionComboBox_SelectionChanged"
SelectedValuePath="Key"
DisplayMemberPath="Value"
SelectedItem="{Binding Path = SelectedOption}"
ItemsSource="{Binding Path = Options}" />
This works fine.
My queries:
1. I am not able to set the initial value to a combo box.
In above XAML snippet the line
SelectedItem="{Binding Path = SelectedOption}"
is not working. I have declared SelectOption in my viewmodel. This is of type string and I have intialized this string value in my view model as below:
SelectedOption = Options[Options.Enter_Value].ToString();
2. The combobox is binded to datadictionary which have two options first is "Enter_value" and second is "Select_value" which is actually Option enum.
Based on the Option enum value I want to perform different action.
For example
if option is equal to option.Enter_value then
Combo box becomes editable and user can enter the numeric value in it.
if option is equal to option.Select_value value then
the value comes from the database and the combo box becomes read only and shows the fetched value from the database.
Please Help!!
Try binding SelectedValue, not SelectedItem if SelectedOption is of type Option.
About your second question: Based on selection you can hide your ComboBox and display a TextBlock or TextBox in it's place. Or you can use RadioButtons and enable or disable input accordingly.
Your problem, probably, is that you've bound SelectedItem to a property of the wrong type.
An ItemsControl iterates over its ItemsSource's enumerator to build its list of items. The enumerator for your dictionary is of type KeyValuePair<Option, string>. So your SelectedOption property must also be of that type - if you look in the Output window when your application is running, you'll probably see a data-binding error to that effect there.
I can't understand your second question.
Edit
Okay, it's a lot easier to just provide a working example than to explain why code that I can't see isn't working.
First, you need a view model class that implements INotifyPropertyChanged and that exposes SelectedItem, Value, and IsValueReadOnly properties, and that correctly raises PropertyChanged events for those properties when the selected item changes.
public enum Option
{
EditOption,
OtherOption
} ;
public class MyViewModel : INotifyPropertyChanged
{
private Dictionary<Option, string> _Items;
private KeyValuePair<Option, string> _SelectedItem;
private string _Value;
public MyViewModel()
{
_Items = new Dictionary<Option, string>
{
{Option.EditOption, "Editable value"},
{Option.OtherOption, "Other value"}
};
}
public Dictionary<Option, string> Items
{
get { return _Items; }
}
public KeyValuePair<Option, string> SelectedItem
{
get { return _SelectedItem; }
set
{
_SelectedItem = value;
OnPropertyChanged("SelectedItem");
OnPropertyChanged("IsValueReadOnly");
OnPropertyChanged("Value");
}
}
public bool IsValueReadOnly
{
get { return _SelectedItem.Key != Option.EditOption; }
}
public string Value
{
get { return IsValueReadOnly ? "Read-only" : _Value; }
set { _Value = value; }
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler h = PropertyChanged;
if (h != null)
{
h(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Now the XAML for your ComboBox and TextBox looks like this:
<Window x:Class="WpfApplication6.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:WpfApplication6="clr-namespace:WpfApplication6"
Title="MainWindow">
<Window.DataContext>
<WpfApplication6:MyViewModel/>
</Window.DataContext>
<StackPanel>
<ComboBox ItemsSource="{Binding Items}"
DisplayMemberPath="Key"
SelectedItem="{Binding SelectedItem}"/>
<TextBox Text="{Binding Value}"
IsReadOnly="{Binding IsValueReadOnly}"/>
</StackPanel>
</Window>