Open amine2050 opened 1 month ago
The UserControl
property needs to have OneWay
mode as well.
The
UserControl
property needs to haveOneWay
mode as well.
Sorry, can you explain what you mean?
Sorry, I went with 8 years of professional WPF experience but probably also accounted for telepathy:
In MyCustomInput.xaml
, adjust the binding to TextBox.Text
property with Mode=OneWay
.
https://source.dot.net/#PresentationFramework/System/Windows/Controls/TextBox.cs,666 Text
property binds TwoWay
by default, so I think you know what happens there.
If you set UpdateSourceTrigger=PropertyChanged
on the binding and type into the TextBox
without setting it as OneWay
, you will notice the binding is thrown away instantly and has nothing to do with you updating the backing property programmatically.
Thanks for explaining! 😅 telepathy didn't work as expected
The TwoWay
binding between TextBox.Text
and MyCustomInput.Value
was intended.
That is my "internal technique" inside of MyCustomInput
to update Value
. I want Value
to be internally synchronized with the TextBox, but not necessarily with the external consumers (ViewModel). I even tried updating Value
manually in code (without using a TwoWay
binding), but got same "broken binding" behavior.
Using PropertyChanged
or on LostFocus
as trigger does not matter, you'll get the binding broken as soon as you try to update the DependencyProperty Value
.
Needless to say this is just a sample to reproduce the issue, not my productive code, so it does not make a lot of sense on first sight.
I just wanted to show you with the trigger that you updating the property via code-behind has no effect on whether it breaks or not (so the "reproduction" steps could have been simplified).
Your "internal technique" has a basic design flaw if I understand you correctly, and that's you wanting to have TwoWay
(TextBox.Text
<->MyCustomInput.Value
) binding on one side but OneWay
(MyCustomInput.Value
<-TheValue
) onto the other side with one property. That's not really OneWay
if you look at it from the property perspective, is it.
You're gonna need two dependency properties (one for each binding, TwoWay for TextBox, OneWay for your case) and handle the "OneWay" synchronization via SetValue
inside PropertyChangedCallback
on the other, that's the best I can think of rn for this. At least you didn't want generics 😀
What you understood is correct: In my sample, I want to have this:
TextBox.Text
<->MyCustomInput.Value
<- TheValue
However, the internal binding (TextBox.Text
<->MyCustomInput.Value
) does not matter, it is just how MyCustomInput.Value
gets updated in the example.
My requirement in more simple words:
I have a custom Control/UserControl with an exposed DependencyProperty Value
that may change its value by some internal logic. As a consumer of that control, I want to bind Value
to my ViewModel (or other DependencyProperty) using OneWay
binding, so that changes in my VM are reflected in the Control, but changes from the control itself do not propagate back to my ViewModel.
The predefined TextBox in WPF perfectly fulfills this simple requirement with its DP Text
. However when trying to implement this on my own in a straightforward way, the first change on the DP breaks the binding silently!
Is the TextBox doing some special magic to prevent breaking the binding?
Is this a desired behavior of WPF OneWay
binding? Any documentation/literature?
(PS: I'll give it a try later with 2 DPs, but I don't think that would make a difference, I already tried setting the DP value in code instead of the binding, no chance)
There's no magic, the difference is that your VM property is not a DependencyProperty
but a raw CLR POCO, you're mixing up binding DP-DP and DP-POCO.
Here's the working example for your desired effect, this way TextBox
is living its life, changes propagated into TextBound
and once VMBound
changes, it is propagated into TextBound
/TextBox.Text
.
For clarity I've noted the binding modes in markup and named the properties accordingly.
MyCustomInput.xaml.cs:
public static readonly DependencyProperty VMBoundProperty = DependencyProperty.Register(
nameof(VMBound), typeof(string), typeof(MyCustomInput),
new FrameworkPropertyMetadata(default(string), (d, e) => { d.SetValue(TextBoundProperty, e.NewValue); Debug.WriteLine($"VM-Old: {e.OldValue}"); Debug.WriteLine($"VM-New: {e.NewValue}"); }));
public string VMBound
{
get => (string)GetValue(VMBoundProperty);
set => SetValue(VMBoundProperty, value);
}
public static readonly DependencyProperty TextBoundProperty = DependencyProperty.Register(
nameof(TextBound), typeof(string), typeof(MyCustomInput),
new FrameworkPropertyMetadata(default(string), (d, e) => { Debug.WriteLine($"Text-Old: {e.OldValue}"); Debug.WriteLine($"Text-New: {e.NewValue}"); }));
public string TextBound
{
get => (string)GetValue(TextBoundProperty);
set => SetValue(TextBoundProperty, value);
}
MyCustomInput.xaml:
<TextBox Text="{Binding ElementName=UserControl, Path=TextBound, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
MyView.xaml:
<lostBindingSample:MyCustomInput x:Name="MyCustomInput" VMBound="{Binding TheValue, Mode=OneWay}"/>
Thanks a lot for the working example! I see now what you mean by using 2 DPs. You propagate the OneWay
binding manually into the "internal" DP, I first thought you meant the other way around, which would cause the same issue. Got that!
I consider this proposition an alternative workaround for the problem.
The requirement is having one single exposed DP that can be bound either OneWay
or TwoWay
(sorry didn't mention the latter explicitly) depending on the consumer of the control, just like the WPF TextBox.Text
property.
I am more concerned about understanding the theoretical root cause of why the OneWay
binding breaks and if it is a documented expected behavior, than finding a workaround for my current problem.
A very interesting point about TextBox
when binding Text
DP using OneWay
binding:
When you edit the text via TextBox
UI, the property Text
gets instantly updated without breaking the binding. However, when you try setting the TextBox.Text
value via code, the same "broken binding" behavior occurs.
This is what I called previously "some special magic". I'll try to dig into TextBox code to find out how that works.
Don't dig into TextBox
but dig into BindingExpression
documentation because setting the value explicitly breaks this.
And possibly into understanding the differences between SetValue
and SetCurrentValue
(and why do they exist).
Same as what happens when you attempt to have a single DependencyProperty
operate in two exclusive modes.
And one more EDIT: It breaks because the expression tries to assign a value to DependencyProperty
in OneWay
mode, which fails, and therefore it detaches.
Thanks for bringing up SetCurrentValue()
, I just dug into TextBox
code and found out it is internally using SetCurrentValue()
(actually an internal SetCurrentDeferredValue()
).
To summarize (correct me if I am wrong):
SetValue()
would break/replace existing OneWay
binding on the DP, but keeps existing TwoWay
binding (therefore the confusion) and just propagates the new value.SetCurrentValue()
does not break existing bindings, it just alters the DP value and let the eventual TwoWay
binding propagate back to source.SetValue()
, which would break eventual existing OneWay
binding on the DP.That explains that this Binding chain has a "design flow" (as you mentioned):
TextBox.Text <-> MyCustomInput.Value <- TheValue
So, either this:
TextBox.Text <- MyCustomInput.Value <- TheValue
or this:
TextBox.Text <-> MyCustomInput.Value <-> TheValue
If all of this is correct, this introduces new rules to write custom Control/UserControl that does not accidentally break eventual OneWay
bindings when used:
SetValue()
to set a property of an "exposed" DP, use SetCurrentValue()
insteadTwoWay
bindings to update the value of your DP, because that might break eventual existing OneWay
binding.Unfortunately, these rules, despite being rational, seems to be unnatural and would complicate writing custom controls and bloat the code.
Alternatively, we do not introduce any new rules. Instead of that we make our WPF developers aware, that OneWay
bindings are "easy to break" and should be used with caution only with "pure input" DPs.
Actually, I barely use OneWay
bindings on properties that might change from inside the control, OneWay
bindings are normally for "read only" information that you feed your control with.
What a nice summary you've made, this would be pretty much a good answer to someone. Unluckily I don't have that gift, haha.
Yeah, based on your models, there's also OneWayToSource
that does "->" as you surely know (which has also its funny but expected quirk, yet may cause confusion for some from my experience: if you have a default value on ur POCO, then while it globs on that value, it will be replaced by whatever the DP has right after init, so there's no way to ever retrieve that default value on this specific binding without replacing it).
SetValue
indeed works that way, the TwoWay
binding will survive, which may cause confusion.
I do not agree with the "new rules", you should be aware how binding expressions work and what may break it.
Imho what should be said is; use SetCurrentValue
if you're touching any DP (exposed) externally (unless you intend to replace/update binding), and preferrably use SetValue
internally if value update is what you're after.
The main takeaway is, don't expect one DP to work in two exclusive modes as it was expected in this case. Binding between DPs is not the same as binding between POCO property and a DP:
I would also say, the awareness of the "breakability" of OneWay
bindings is the way to go. Although it is a delicate topic and not easy to fully explain to newbies.
I like always to have clear best practices to "enforce" within my team to avoid such "silent" problems. But I think this is not the case.
To close up our discussion:
WPF bindings have the following behaviors that should be known by developers
TwoWay
binding may break a OneWay
bindingSetProperty()
would detach OneWay
binding but not TwoWay
bindingAlthough these behaviors are somehow confusing and can theoretically be improved, I see nothing that may be touched here in the deepest WPF codes. We do not want to break half the world again 🤣
Nevertheless, I suggest adding a binding Error/Warning that can be seen in Output and in the "XAML Binding Failures" window in case a OneWay
binding has been broken/detached by a TwoWay
binding update. Probably also when it gets detached by using SetProperty()
(maybe just a Warning here).
Such an Error/Warning would be very helpful for every developer struggling with such broken bindings. Since we have no clear best practices to 100% eliminate the risk.
What do you think?
The tracing could be theoretically improved as to reason why the detach has happened. Touching anything around that is a thin ice though, it still uses goto
statements, haha.
Documentation could be improved as to what is by design, I agree.
Description
Hi,
I've been writing advanced WPF stuff for the last 8 years. This is the first time I notice this odd behavior of One-Way bindings in such a basic use case:
The One-Way binding is cleared by the WPF engine when the target Dependency Property (in a custom Control/UserControl) is internally updated (in code or via internal binding).
I prepared a very simple sample to reproduce that, to which I added a
BindingObserver
control that continuously outputs the state of theBindingExpression
on the UI.Here is the sample: https://github.com/amine2050/wpf-lost-binding
Am I missing a basic knowledge of how One-Way bindings behave? Or is this a known issue? I could not find any literature about the topic.
Reproduction Steps
Expected behavior
One-way binding to ViewModel is expected to continue working even after changing the value of the DependencyProperty (by changing TextBox content).
The new content of the TextBox should not propagate to the binding Source (which is correct here), but changes in the Source are expected to be reflected on the TextBox (this does not happen anymore).
I want the same behavior of a simple TextBox bound to ViewModel using One-Way binding.
Actual behavior
One-way binding to ViewModel has been cleared by the WPF engine without any warnings or output. Changes in ViewModel are no loger propagating to UI.
Regression?
I tested this with .NET 8.0 and .NET Framework 7.2.1 and got the same behavior. So no regression is known.
Known Workarounds
The only workaround I found is to use Two-Way binding instead of One-Way. But this is not necessarily what I want actually.
Impact
This applies to any custom Control/UserControl that has properties you want to change internally while allowing binding them using One-Way binding.
Configuration
Other information
I added
PresentationTraceSources.TraceLevel=High
to the binding and got the following output when the binding gets broken: