I'm making a small application, it's a form that reads from a data source, I want to use it for editing and adding new records.
so the default binding mode for the textboxes in the form is TwoWay mode, so the user can edit an existing record, but I want to add a Checkbox that when checked, it marks the data in the textboxes as new, and then adding them to the data source, so I need to change the binding mode to OneWay,
to my knowledge, to do this in code I need to create a new Binding object, that I will have to set properties like Source that doesn't change:
Binding myBinding = new Binding();
myBinding.Source = ViewModel;
myBinding.Path = new PropertyPath("SomeString");
myBinding.Mode = BindingMode.TwoWay;
myBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
So is there a way to change only the Binding mode in code?
EDIT
some further explanation of the application:
In the form there's a combobox that is bound to a List<Book>, there are 3 TextBoxs, their Text properties bound to the DataContext object of their container which itself set to the SelectedItem of the Combobox.
When I added the ReadOnly property as described in the answer, when I check the checkbox I can't change the text in the textboxes.
..
Thanks!
Don't change Mode of binding. Just correct your view-model logic.
public class ViewModel : INotifyPropertyChanged
{
private string _text;
private bool _readOnly;
public string Text
{
get { return _text; }
set
{
if (ReadOnly || value == _text)
return;
_text = value;
OnPropertyChanged(nameof(Text));
}
}
public string ReadOnly
{
get { return _readOnly; }
set
{
if (value == _readOnly)
return;
_readOnly = value;
OnPropertyChanged(nameof(ReadOnly));
}
}
}
In XAML bind IsChecked property of your CheckBox to ReadOnly property.
The answer to this is "Yes, but". The Mode property of Binding has a setter. So it appears that you can set the mode of an existing binding like so...
BindingExpression be = textBox.GetBindingExpression(TextBox.TextProperty);
Binding b = be?.ParentBinding as Binding;
if (b != null)
{
b.Mode = BindingMode.OneWay;
}
However if you do this you will get in every case the exception...
System.InvalidOperationException occurred
HResult=0x80131509
Message=Binding cannot be changed after it has been used.
So the only way to accomplish what you want is to create a new binding based on the old binding while changing the mode. Then replace the old binding.
BindingExpression be = textBox.GetBindingExpression(TextBox.TextProperty);
Binding b = be?.ParentBinding as Binding;
if (b != null)
{
Binding b2 = new Binding();
b2.Path = b.Path;
b2.Mode = BindingMode.OneWay;
textBox.SetBinding(TextBox.TextProperty, b2);
}
This is less than ideal because for completeness you would need to copy the converter, the converter parameter and so on, but this is the best you can do.
Related
I have a custom control dynamically created by another control, I want to change its VisualState based on the VisualState of the parent.
VisualState is a DependencyProperty that accept an enumerator, the control internally uses it inside the OnPropertyChange event to change size and internal layout.
The property are made identical on both controls (of course except the type).
public ControlSize VisualState
{
get { return (ControlSize)GetValue(VisualStateProperty); }
set
{
if (value != VisualState)
{
SetValue(VisualStateProperty, value);
}
}
}
public static readonly DependencyProperty VisualStateProperty = DependencyProperty.RegisterAttached(nameof(VisualState), typeof(ControlSize), typeof(CountersListControl), new PropertyMetadata(ControlSize.Large, OnVisualStateChanged));
The parent control dynamically allocate the component and binds its VisualState to the new control VisualState:
CounterControl cc = new CounterControl();
cc.SetBinding(CounterControl.ValueProperty, new Binding() { Path = new PropertyPath(nameof(Counter.Amount)), Source = counter, Mode = BindingMode.TwoWay });
//cc.DataContext = this;//I tried with it, but it doesn't change a thing
cc.SetBinding(CounterControl.VisualStateProperty, new Binding() { Path = new PropertyPath(nameof(VisualState)), Source = this, Mode = BindingMode.OneWay });
The Value property binds without any issue to Counter.Amount, and looks that VisualState does too.
BUT the OnVisualState method is called when the parent is changed, while the children value is not.
UPDATE: I debugged the binding as suggested by #EdPlunkett, and I was getting the following message:
Error: Converter failed to convert value of type 'Windows.Foundation.Int32' to type 'ControlSize';
ControlSize is an enumerable, so it should be able to convert it.
This happens because somehow it can't convert an Int32 into an enumerable (even if the source is the same enumerable).
I solved creating an IValueConverter that converts Int32/ControlSize types and assigning it to the binding.
Binding visualStateBinding = new Binding() { Path = new PropertyPath(nameof(VisualState)), Source = this, Mode = BindingMode.OneWay, Converter = new ControlSizeConverter() };
I have a ObservableCollection<List<MessageView>> (MessageView is a custom class) I instantiate it that way
public ObservableCollection<List<MessageView>> _messagesView;
public ObservableCollection<List<MessageView>> messagesView {
get {
if (_messagesView == null) {
_messagesView = new ObservableCollection<List<MessageView>>();
}
return _messagesView;
}
set {
if (_messagesView != value) {
_messagesView = value;
PropertyChanged(this, new PropertyChangedEventArgs(nameof(messagesView)));
}
}
}
This property is set on a Singleton
I want to bind one of the item collection to a datagrid it would look that way in xaml:
<xmlns:module="clr-namespace:Myproject.MyNameSpace;assembly=Myproject">
<DataGrid
Name="DataGrid_messages"
...
ItemsSource="{Binding messagesView[2], Source={x:Static module:Singleton.Instance}}"
>
This is working well that way but this is not what I want to do. I want to have the control of my index. So I have to do the binding in c# with my controller but I never found an example to bind with a special index.
Binding myBinding = new Binding("messagesView");
myBinding.Source = Singleton.Instance;
myBinding.Path = ??
DataGrid_messages.SetBinding(DataGrid.ItemsSourceProperty, myBinding);
Share your thought about this, is it possible? Or a better way to do it?
UPDATE
Additional change to do with Clemens Answer:
The binding is set with the internal list so it's it which should be ObservableCollection type:
public List<ObservableCollection<MessageView>> messagesView;
Provided that the index is fixed, creating the binding path in code behind could look like this:
myBinding.Path = new PropertyPath(string.Format("messagesView[{0}]", index));
I am working on a WPF application and i have a textbox bound (bidirectionally) to a property in my view model.
I am trying to prevent a user from typing more than 100 characters into this textbox (this is the max the database will store) so i have written this.
public abstract class AppBaseViewModel : ViewModelBase
{
private String _text;
public String Text
{
get { return _text; }
set
{
_text = CheckTextLength(value, _text);
OnPropertyChanged("Text");
}
}
private string CheckTextLength(string value, string text)
{
if (value.Length < 100)
{
return value;
}
else
{
return text;
}
}
}
All this code seems to do is save the first 100 characters to the field but it still allows the user to carry on typing past 100 characters... i would guess it is because the field value isn't being passed back to the textbox.
I don't understand why this doesn't work as i did something similar using MVVM Light's RaisePropertyChange() in a different application.
It is worth noting that i am unable to access the designer for the textbox so cannot set the .Net textbox property for max length.
Edit: Just for clarification i cannot view or edit the xaml as some are suggesting as i do not have access to the XAML file (i know, it's stupid). All the bindings we use are two way by default
Have you tried with TextBox.MaxLength ?
<TextBox MaxLength="100"/>
Gets or sets the maximum number of characters that can be manually entered into the text box.
If no access to the XAML, eventually get access to the XAML instead of parsing and verifying lengths of arrays and use substrings here and there. At least that's what i would do for this simple issue or talk to the designer to add that small piece of code.
Update 1
public static T GetChildOfType<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj == null) return null;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = (child as T) ?? GetChildOfType<T>(child);
if (result != null) return result;
}
return null;
}
Go and get that child and set its MaxLength. This is just a slight modification on the View so it will not affect the MVVM pattern.
OK. I'm not at all sure that I'm proud of this, but am presenting it as an alternative.
You can change the UpdateSourceTrigger of the TextBox's Text property by applying a universal Style to all of the TextBoxes. This is only going to be practical in pretty weird arrangements, but the question is a little unusual in itself.
XAML codebehind:
//I'm using MVVM Light here - you need to be able to find an instance
//of your AppBaseViewModel somehow.
private ViewModelLocator _locator;
//View codebehind constructor, may need to change names as appropriate
public AppBaseView()
{
InitializeComponent();
//MVVM Light again
_locator = new ViewModelLocator();
//Create the binding
Binding binding = new Binding();
//Source = The instance of your ViewModel
binding.Source = _locator.AppBaseViewModel ;
binding.Path = new PropertyPath("Text");
binding.Mode = BindingMode.TwoWay;
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
//Create a Style with no Key - this will apply to *all* TextBoxes
//without their own explicit Style set.
Style style = new Style(typeof(TextBox));
style.Setters.Add(new Setter(TextBox.TextProperty, binding));
//Add the Style to the XAML's Resources:
Resources.Add(typeof(TextBox), style);
}
The view won't listen to the PropertyChanged notification if it's currently trying to change the property itself.
The only thing that comes to mind is launching an extra delayed PropertyChanged notification when you detect the constraint is not met...
private string CheckTextLength(string value, string text)
{
if (value.Length < 100)
{
return value;
}
else
{
MyDispatcher.BeginInvoke(new Action(() =>
OnPropertyChanged("Text")),
DispatcherPriority.Loaded);
return text;
}
}
Can't try the code, so sorry if it doesn't build righ away. MyDispatcher could be your Application.Current.Dispatcher, for instance.
The xaml view /the binding is only updated when the textbox has lost focus. if the text entered is <100 then the value is set otherwise _text is set. this means that initially _text has no value so null will be set upon the if statement being false. i also suggest yo use RaisePropertyChanged(); and when used within the property itself no parameter is needed.
I am creating WPF elements dynamically in code behind, and for each of the rows in the Grid I'm building it consists of a CheckBox and a Dynamic number of TextBoxes. The interaction that is needed is the following:
If all TextBoxes in a row have a value of 0, set the CheckBox
IsChecked property to true and Disable it.
If one of the TextBoxes is then changed from 0, enable the
CheckBox and set IsChecked to false.
If the user clicks on the CheckBox, set all associated TextBoxes
to 0 and Disable the CheckBox
I was able to accomplish the first part of the last one using this code:
Binding setScoreToZeroIfIsNormalChecked = new Binding("IsChecked");
setScoreToZeroIfIsNormalChecked.Source = this.NormalCheckBoxControl;
setScoreToZeroIfIsNormalChecked.Converter = m_NormalCheckBoxJointScoresConverter;
tempJointScoreControl.JointScoreContainer.SetBinding(ContainerBase.SingleAnswerProperty, setScoreToZeroIfIsNormalChecked);
and the converter:
public object Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is bool && targetType == typeof(Answer))
{
if ((bool)value)
{
Answer answer = new Answer();
answer.Value = "0";
answer.DisplayValue = "0";
return answer;
}
else
return null;
}
else
{
return null;
}
}
However, in attempting to create another converter to accomplish other functionality, I was running into issues of converters stepping on one another since all functionality is based around the CheckBox.IsChecked property.
Is there anyway to accomplish all of the above using one or two multibinding converters? I'd really like to avoid having to create a whole bunch of events and maintaining them in order to do this.
It's relatively easy. Everything should resolve around CheckBox IsChecked property.
For a simple reason, it's a two-way property. So either you can modify it, or CheckBox can modify it.
So what you do, you use MultiBinding, as such:
MultiBinding multiBinding = new MultiBinding();
multiBinding.Converter = multiBindingConverter;
multiBinding.Bindings.Add(new Binding("Text") { Source = txtbox1});
multiBinding.Bindings.Add(new Binding("Text") { Source = txtbox2});
multiBinding.NotifyOnSourceUpdated = true;//this is important.
checkBox.SetBinding(CheckBox.IsCheckedProperty, multiBinding);
And in your multiBindingConverter, you will have object[] value as first parameter, which you need to convert into IList and iterate over it && do your calculations, if you should either return true/false.(IsChecked=true or false)
Now bind CheckBox IsEnabled to CheckBox IsChecked property, and use BooleanInverterConverter. (If CheckBox is checked, it should be disabled, and vice versa)
The last step is to make TextBoxes listen to actual IsChecked property of CheckBox.
If it is TRUE, they all should show value of 0, otherwise they can show what they want.
So, make a new MultiBinding.
MultiBinding multiBinding = new MultiBinding();
multiBinding.Converter = textboxMultiBindingConverter;
multiBinding.Bindings.Add(new Binding("IsChecked") { Source = checkbox1});
multiBinding.Bindings.Add(new Binding("Text") { Source = textbox1});
multiBinding.NotifyOnSourceUpdated = true;//this is important.
textbox1.SetBinding(TextBox.Text, multiBinding);
the idea in textboxMultiBindingConverter is to either return Text(value[1]) if value[0]==FALSE or "0" if value[0]==TRUE.
This problem can be solved very easily if you would use MVVM.
You would have a ViewModel that represents a row in the grid. It would have a property per textbox and one for the checkbox.
Additionally you would have a ViewModel for the View containing the Grid and this ViewModel would expose a collection of row ViewModels.
The ViewModel for your row:
public class AnswersViewModel : ViewModelBase // From MvvmLight
{
public bool IsAnswered
{
get { return _isAnswered; }
set
{
if(value == _isAnswered)
return;
_isAnswered = value;
if(_isAnswered)
{
Answer1 = "0";
Answer2 = "0";
}
RaisePropertyChanged("IsAnswered");
}
}
public string Answer1
{
get { return _answer1; }
set
{
if(value == _answer1)
return;
_answer1 = value;
RaisePropertyChanged("Answer1");
if(_answer1 == "0" && _answer2 == "0")
{
_isAnswered = true;
RaisePropertyChanged("IsAnswered");
}
}
}
// The implementation of Answer2 is similar to Answer1
}
The ViewModel for the View:
public class FooViewModel : ViewModelBase
{
public ObservableCollection<AnswersViewModel> Answers
{
get { return _answers; }
}
}
Your View would contain the Grid with ItemsSource="{Binding Answers}" and a ControlTemplate for the items which binds to the properties of AnswersViewModel.
Disabling the CheckBox I would handle via a Trigger in a Style.
I have the need to set a binding in code.
I can't seem to get it right tho.
This is what i have tried:
XAML:
<TextBox Name="txtText"></TextBox>
Code behind:
Binding myBinding = new Binding("SomeString");
myBinding.Source = ViewModel.SomeString;
myBinding.Mode = BindingMode.TwoWay;
myBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
BindingOperations.SetBinding(txtText, TextBox.TextProperty, myBinding);
ViewModel:
public string SomeString
{
get
{
return someString;
}
set
{
someString= value;
OnPropertyChanged("SomeString");
}
}
The property is not updating when i set it.
What am i doing wrong?
Replace:
myBinding.Source = ViewModel.SomeString;
with:
myBinding.Source = ViewModel;
Example:
Binding myBinding = new Binding();
myBinding.Source = ViewModel;
myBinding.Path = new PropertyPath("SomeString");
myBinding.Mode = BindingMode.TwoWay;
myBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
BindingOperations.SetBinding(txtText, TextBox.TextProperty, myBinding);
Your source should be just ViewModel, the .SomeString part is evaluated from the Path (the Path can be set by the constructor or by the Path property).
You need to change source to viewmodel object:
myBinding.Source = viewModelObject;
In addition to the answer of Dyppl, I think it would be nice to place this inside the OnDataContextChanged event:
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
// Unforunately we cannot bind from the viewmodel to the code behind so easily, the dependency property is not available in XAML. (for some reason).
// To work around this, we create the binding once we get the viewmodel through the datacontext.
var newViewModel = e.NewValue as MyViewModel;
var executablePathBinding = new Binding
{
Source = newViewModel,
Path = new PropertyPath(nameof(newViewModel.ExecutablePath))
};
BindingOperations.SetBinding(LayoutRoot, ExecutablePathProperty, executablePathBinding);
}
We have also had cases were we just saved the DataContext to a local property and used that to access viewmodel properties. The choice is of course yours, I like this approach because it is more consistent with the rest. You can also add some validation, like null checks. If you actually change your DataContext around, I think it would be nice to also call:
BindingOperations.ClearBinding(myText, TextBlock.TextProperty);
to clear the binding of the old viewmodel (e.oldValue in the event handler).
Example:
DataContext:
class ViewModel
{
public string SomeString
{
get => someString;
set
{
someString = value;
OnPropertyChanged(nameof(SomeString));
}
}
}
Create Binding:
new Binding("SomeString")
{
Mode = BindingMode.TwoWay,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
};