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() };
Related
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.
I have an object that I have bound to a control on a form using C# WinForms (targetting .NET 4.5.2). I have implemented INotifyPropertyChanged, and when I modify a Property of this object, it updates on the Form's control as expected. However, when I change this object's instance to a new instance, it will no longer update the control, even if I try to modify a specific Property.
class User : INotifyPropertyChanged
{
string _name = "";
public string Name
{
get { return _name; }
set { _name = value; OnPropertyChanged("Name"); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
public User() { }
public User(string name)
{
Name = name;
}
public User(User otherUser)
{
Name = otherUser.Name;
}
}
and on the Form I have
User currentUser = new User("Example Name");
lblName.DataBindings.Add("Text", currentUser, "Name");
which updates properly. I can change name using currentUser.Name = "Blahblah"; and it works fine. When I try to call currentUser = new User("New Name");, it will no longer update no matter what I do.
It is my understanding that the specific instance of the object is what the controls are bound to. My primary goal is to not have to manually go through each Property of larger objects and manually change everything over every single time I want to change instances.
My question: is there a way to change instances of an object without removing the binding to the control?
To get the desired behavior, you should not bind directly to the concrete instance like you do currently:
lblName.DataBindings.Add("Text", currentUser, "Name");
Instead, you need some intermediary. The easiest is to use the BindingSource component for that purpose.
Add a BindingSource field to the form:
private BindingSource dataSource;
Then initialize it and bind the controls to it one time (usually in form Load event):
dataSource = new BindingSource { DataSource = typeof(User) };
lblName.DataBindings.Add("Text", dataSource, "Name");
// ...
Now anytime you want to bind to a User instance, you simply assign it to the DataSource property of the BindingSource:
initial:
dataSource.DataSource = new User("Example Name");
later:
dataSource.DataSource = new User("New Name");
Ivan showed you how to solve the problem. Here in this answer I'll try to show you what the problem is and why it works this way.
Short answer
Your label is bound to an object not the variable. The variable is just like a pointer to the object. So replacing the variable doesn't have any impact on the object which your label is using as data source. Just the variable points to the new object. But your label is using the previous object. It has its own pointer to the previous object.
Consider these facts to know what is happening:
User currentUser = new User("Example Name");
When you assign an object to a variable, in fact the variable points to the object.
lblName.DataBindings.Add("Text", currentUser, "Name");
You performed data-binding to the object. The control will be bound to the object not the variable.
currentUser.Name = "Blahblah";
You changed the value of Name property and since you implemented INotifyPropertyChanged the control will be notified of changes and will refresh its Text.
But the main statement which didn't work as you expected:
currentUser = new User("New Name");
Here you made the currentUser variable point to another object. It doesn't have anything to do with the previous object which it was pointing to. It just points to another object.
The Binding which you added to the label, still uses the previous object.
So it's normal to not have any notification, because the object which is in use by the label didn't changed.
How the BindingSource solved the problem?
The BindingSource raises ListChanged event when you assign a new object to its DataSource and it makes the BindingObject fetch new value from DataSource and refresh Text property. You can take a look at this post: How to make a binding source aware of changes in its data source?
I'm trying to create a label at runtime and connect it's Content property to another TextBox control which is in my UserControl called MyLabelSettings.
This is what I got so far:
Label currCtrl = new Label();
MyLabelSettings currCtrlProperties = new MyLabelSettings();
// Bindings to properties
Binding binding = new Binding();
binding.Source = currCtrlProperties.textBox_Text.Text;
binding.Path = new PropertyPath(Label.VisibilityProperty);
BindingOperations.SetBinding(currCtrl.Content, Label.ContentProperty, binding);
The last row shows an error which I did not figure out how to solve:
The best overloaded method match for 'System.Windows.Data.BindingOperations. SetBinding(System.Windows.DependencyObject, System.Windows.DependencyProperty, System.Windows.Data.BindingBase)' has some invalid arguments
I have in MyLabelSettings the implementation of INotifyPropertyChanged
which has the following code when the TexBox.Text changes
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
InvokePropertyChanged(new PropertyChangedEventArgs("TextChanged"));
}
Is there a better way to bind these 2? Or am I doing something wrong in this one?
Thanks!
The problem is simpler than you realize:
This:
binding.Source = currCtrlProperties.textBox_Text.Text;
binding.Path = new PropertyPath(Label.VisibilityProperty);
BindingOperations.SetBinding(currCtrl.Content, Label.ContentProperty, binding);
Should be this:
//The source must be an object, NOT a property
binding.Source = currCtrlProperties;
//Since the binding source is not a DependencyObject, we using string to find it's property
binding.Path = new PropertyPath("TextToBind");
BindingOperations.SetBinding(currCtrl, Label.ContentProperty, binding);
Before, you were attempting to bind the value to an object's property via a property. Now, you're binding the value to an object's property via an object (:
Notes:
You are attempting to bind the text of a control that exists in an instance of a class you just made.
MyLabelSettings currCtrlProperties = new MyLabelSettings();
I base this assumption off this line:
currCtrlProperties.textBox_Text.Text;
Which appears to contain a text control of some sort. Rather, you want bind the text of a property that exists in an instance of a class you make, not a control.
I have a DataGrid which is bound to a Collection of Objects. Every DataGridColumn is created in code behind.
The Background of these columns is dependent on different values of the object. I create the background binding in the CellStyle (as it should not override default style's from triggers).
var backgroundBinding = new Binding
{
Converter = new MyBindingConverter(),
ConverterParameter = new MyConverterParameter()
};
cellStyle.Setters.Add(new Setter(Control.BackgroundProperty, backgroundBinding));
As you can see it binds directly to the element. As different value are changing the value of the Columns is updated accordingly, but the converter of the Binding is not called.
I tried calling OnPropertyChanged(null) to show that the object was updated, but sadly this does not work.
Did you try to specify Path for backgroundBinding? Something like:
var backgroundBinding = new Binding
{
Converter = new MyBindingConverter(),
ConverterParameter = new MyConverterParameter(),
ElementName = YourElementName,
Path = PropertyOnElement
};
cellStyle.Setters.Add(new Setter(Control.BackgroundProperty, backgroundBinding));
is it possible to define (not switch) VisualStates in CodeBehind?
I'm creating an Adorner, that draws some rectangles in OnRender. What I'd like to do is to change the Opacity of these Rectangles by it's Property IsMouseOver (say from 0.3 to 0.8).
In any control with a visual tree I'd add some VisualStates and switch those with a DataStateBehavior. How do I do this with an Adorner?
this is entirely possible.
if anyone is interested here is how I did it:
public class MyAdorner: Adorner
{
ctor (...):base(...)
{
...
var storyboard = new Storyboard();
var doubleAnimation = new DoubleAnimation(0.2,new Duration(TimeSpan.Zero));
Storyboard.SetTarget(doubleAnimation,this);
Storyboard.SetTargetProperty(doubleAnimation,new PropertyPath(RectOpacityProperty));
storyboard.Children.Add(doubleAnimation);
var storyboard2 = new Storyboard();
var doubleAnimation2 = new DoubleAnimation(0.5, new Duration(TimeSpan.Zero));
Storyboard.SetTarget(doubleAnimation2, this);
Storyboard.SetTargetProperty(doubleAnimation2, new PropertyPath(RectOpacityProperty));
storyboard2.Children.Add(doubleAnimation2);
var stateGroup = new VisualStateGroup { Name = "MouseOverState" };
stateGroup.States.Add(new VisualState { Name = "MouseOut", Storyboard = storyboard });
stateGroup.States.Add(new VisualState { Name = "MouseOver", Storyboard = storyboard2});
var sgs = VisualStateManager.GetVisualStateGroups(this);
sgs.Add(stateGroup);
var dsb = new DataStateBehavior
{
Value = true,
FalseState = "MouseOut",
TrueState = "MouseOver"
};
BindingOperations.SetBinding(dsb, DataStateBehavior.BindingProperty, new Binding {Source = this, Path = new PropertyPath(IsMouseOverProperty)});
dsb.Attach(this);
}
protected override void OnRender(DrawingContext drawingContext)
{
drawingContext.DrawRectangle(_mouseOverBrush, _pen, _rects[i]); //mouseoverbrush is a Solidcolorbrush
}
public double RectOpacity
{
get { return (double)GetValue(RectOpacityProperty); }
set { SetValue(RectOpacityProperty, value); }
}
public static readonly DependencyProperty RectOpacityProperty =
DependencyProperty.Register("RectOpacity", typeof(double), typeof(XmlNodeWrapperAdorner), new FrameworkPropertyMetadata(0.0,FrameworkPropertyMetadataOptions.AffectsRender,(o, args) =>
{
var adorner = o as MyAdorner;
adorner._mouseOverBrush.Color = Color.FromArgb((byte)((double)args.NewValue * 0xFF), 0xFF, 0xBE, 0x00);
}));
}
pretty straightforward actually.
key points here are:
you cannot set the VisualStateGroups attached property. you have to get the collection and then add your own group
you cannot do new DataStateBehavior{Binding = new Binding(...){...}} as this will assign not bind some value to the property. As Behvior<T> doesn't derive from FrameworkElement you also can't use SetBinding but have to use the BindingOperations class.
for automatic rerendering when the property changes keep in mind to set FrameworkPropertyMetadataOptions.AffectsRender.
Since you're already creating a custom adorner with your own behavior, i would suggest that you override the MouseOver method of the adorner and change the opacity of your rectangles there...
another way would be to listen to your own PropertyChanged event and monitor the change in IsMouseOver, or maybe monitor the MouseMove event...
If you could add States in code, tools such as Blend would have to run all code in all possible configurations to find out what states are present/possible.
So, no, you can't do this in code. It only possible using attributes.
EDIT
I stand corrected but the problem mentioned still remains. This technique is not useful for designers.