I have a large number of buttons that have all the same binding schema based on a key string. I thought I could save some code duplication by making a custom control that takes that string and sets all the bindings accordingly. I came up with the following code:
public class StateTransitionButton : Button
{
public string StateTransition
{
get { return (string)this.GetValue(StateTransitionProperty); }
set { this.SetValue(StateTransitionProperty, value); }
}
public static readonly DependencyProperty StateTransitionProperty =
DependencyProperty.Register("MyProperty", typeof(string), typeof(StateTransitionButton), new PropertyMetadata(null, OnTransitionChanged));
private static void OnTransitionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is StateTransitionButton button && e.NewValue is string key)
{
button.CommandParameter = key;
Binding commandBinding = new("ButtonClicked");
commandBinding.Source = button.DataContext;
_ = button.SetBinding(CommandProperty, commandBinding);
button.CommandParameter = key;
Binding visibilityBinding = new("CurrentState")
{
Converter = new UIStateToVisibilityConverter(),
ConverterParameter = key
};
visibilityBinding.Source = button.DataContext;
_ = button.SetBinding(VisibilityProperty, visibilityBinding);
Binding tooltipBinding = new("CurrentState")
{
Converter = new UIStateToTooltipConverter(),
ConverterParameter = key
};
tooltipBinding.Source = button.DataContext;
_ = button.SetBinding(ToolTipProperty, tooltipBinding);
}
}
}
The converters are already used in the old code and work as intended. In the code above the bindings appear not to be set correctly. When I check them in snoop the command binding has an error (i never figured out how to get usable error text from snoop), visibility still has the default value and not a binding and the tooltip property is not inspectable.
Update: I figured this out as I was finishing up writing it. I will post it anyway and answer it myself because I couldn't find any nice clear examples of how to set up bindings inside a custom control and other might find my solution useful. Please mark this as a duplicate if there is a better one I missed.
So it turns out the StateTransition property was getting set before the datacontext. This means all the bindings were binding onto null. The fix is to listen to the DataContextChanged event and rebind. This is more correct anyway even if the original code had worked because otherwise it would not handle datacontext changes.
Working example:
public class StateTransitionButton : Button
{
public StateTransitionButton()
{
this.DataContextChanged += this.OnDataContextChanged;
}
public string StateTransition
{
get { return (string)this.GetValue(StateTransitionProperty); }
set { this.SetValue(StateTransitionProperty, value); }
}
public static readonly DependencyProperty StateTransitionProperty =
DependencyProperty.Register("MyProperty", typeof(string), typeof(StateTransitionButton), new PropertyMetadata(null, OnTransitionChanged));
private static void OnTransitionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is StateTransitionButton button && e.NewValue is string key)
{
button.SetBindings(key);
}
}
public void SetBindings(string key)
{
this.CommandParameter = key;
Binding commandBinding = new("ButtonClicked");
commandBinding.Source = this.DataContext;
_ = this.SetBinding(CommandProperty, commandBinding);
this.CommandParameter = key;
Binding visibilityBinding = new("CurrentState")
{
Converter = new UIStateToVisibilityConverter(),
ConverterParameter = key
};
visibilityBinding.Source = this.DataContext;
_ = this.SetBinding(VisibilityProperty, visibilityBinding);
Binding tooltipBinding = new("CurrentState")
{
Converter = new UIStateToTooltipConverter(),
ConverterParameter = key
};
tooltipBinding.Source = this.DataContext;
_ = this.SetBinding(ToolTipProperty, tooltipBinding);
}
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
this.SetBindings(this.StateTransition);
}
}
Related
I am facing an issue with my code below where the call back is getting called only the first time DocText is set. Subsequently setting the DocText is not calling my callback. I am trying to reset the html of the browser by setting the same string to the DocText property.
WebBrowser class
public static readonly DependencyProperty DocumentTextProperty =
DependencyProperty.Register("DocumentText", typeof(string),
typeof(WebBrowser), new
FrameworkPropertyMetadata(string.Empty,TextChangedCallback));
private static void TextChangedCallback(DependencyObject
dependencyObject, DependencyPropertyChangedEventArgs
dependencyPropertyChangedEventArgs)
{
var control = (WebBrowser)dependencyObject;
control._browser.DocumentText = dependencyPropertyChangedEventArgs.NewValue.ToString();
}
public string DocumentText
{
get { return (string)GetValue(DocumentTextProperty); }
set { SetValue(DocumentTextProperty, value); }
}
Factory class that creates the instance of the web browser is using the browsers set binding to bind the dependency property to "DocText"
if (!String.IsNullOrEmpty(documentTextProperty))
{
browser.SetBinding(WebBrowser.DocumentTextProperty, documentTextProperty);
}
And the documentTextProperyt is being set like this from the ViewModel class
DocText = "some html string";
public string DocText
{
get
{
_docText = if(html != null? html: string.empty);
return _docText;
}
set
{
docText = value;
OnPropertyChanged(() = DocText);
}
}
Looks like the Notification is not going back to control(WebBrowser). Please check whether INotifyPropertyChanged is properly implemented. I checked equivalent code in my system and it is properly working.
public class MaskedTextBox : TextBox
{
public static readonly DependencyProperty DocumentTextProperty =
DependencyProperty.Register("DocumentText", typeof(string),
typeof(MaskedTextBox), new PropertyMetadata(default(string), TextChangedCallback));
private static void TextChangedCallback(DependencyObject
dependencyObject, DependencyPropertyChangedEventArgs
dependencyPropertyChangedEventArgs)
{
var control = (MaskedTextBox)dependencyObject;
control.Text= dependencyPropertyChangedEventArgs.NewValue.ToString();
}
public string DocumentText
{
get { return (string)GetValue(DocumentTextProperty); }
set { SetValue(DocumentTextProperty, value); }
}
}
<StackPanel Margin="50,5,5,50">
<local:MaskedTextBox x:Name="text1" DocumentText="{Binding ElementName=text2, Path=Text, Mode=OneWay}"/>
<TextBox x:Name="text2"/>
</StackPanel>
Here if you edit second text box it is updating first one by calling the callback method properly!
I'm doing some training project right now. It's supposed to convert numbers into different strings.
Heres the converted Control, and in the bottom way I use it in my Main Window.
So the first problem is that I want to create instance of converter based on value I pass to OutputFormatProperty so in this case I create converter that should be type OctalConverter but instead I get the default one, why is that?
Another thing is that I wan't to change InputValue in the converter by binding it to CurrentValue, which works with NotifyPropertyChanged, but it doesn't seem to work that way.
public partial class ConverterDisplay : UserControl {
private const int DEFAULT_INPUT_VALUE = 0;
private readonly ObservableCollection <DisplayField> _displayFields;
private AbstractNumberConverter _converter;
public static readonly DependencyProperty InputValueProperty = DependencyProperty.Register (
"InputValue",
typeof(int),
typeof(ConverterDisplay),
new PropertyMetadata (DEFAULT_INPUT_VALUE));
public static readonly DependencyProperty OutputFormatProperty = DependencyProperty.Register (
"OutputFormat",
typeof(NumberSystems),
typeof(ConverterDisplay),
new FrameworkPropertyMetadata (NumberSystems.Binary));
public int InputValue {
get {
return (int) GetValue (InputValueProperty);
}
set {
SetValue (InputValueProperty, value);
UpdateDisplay ();
}
}
public NumberSystems OutputFormat {
get {
return (NumberSystems) GetValue (OutputFormatProperty);
}
set {
SetValue (OutputFormatProperty, value);
}
}
public ObservableCollection <DisplayField> DisplayFields {
get { return _displayFields; }
}
public ConverterDisplay () {
_displayFields = new ObservableCollection<DisplayField> ();
InitializeComponent ();
CreateConverter ();
}
private void UpdateDisplay () {
var convertedNumberString = _converter.GetString (InputValue);
if (_displayFields.Count > convertedNumberString.Length)
ResetDisplayFields ();
while (_displayFields.Count < convertedNumberString.Length)
AddDisplayField ();
UpdateValues (convertedNumberString);
}
private void UpdateValues (string convertedString) {
if (_displayFields.Count == 0) return;
for (int i = 0; i < _displayFields.Count; i++) {
_displayFields [i].NumberValue = convertedString [i];
}
}
private void AddDisplayField () {
_displayFields.Insert (
0,
new DisplayField ((int)OutputFormat, _displayFields.Count));
}
private void ResetDisplayFields () {
_displayFields.Clear ();
}
private void CreateConverter () {
switch (OutputFormat) {
case NumberSystems.Binary:
_converter = new BinaryConverter ();
break;
case NumberSystems.Octal:
_converter = new OctalConverter ();
break;
case NumberSystems.Hexadecimal:
_converter = new HexadecimalConverter ();
break;
}
}
}
public enum NumberSystems {
Binary = 2,
Octal = 8,
Hexadecimal = 16
}
And then in the Main Window I'm trying to use that control
<converters:ConverterDisplay x:Name="octConverter"
InputValue="{Binding ElementName=Window,Path=CurrentValue}"
OutputFormat="Octal"/>
Just in case
public int CurrentValue {
get { return _currentValue; }
set {
if (value == _currentValue)
return;
ValidateNewValue (value);
OnPropertyChanged ();
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged ([CallerMemberName] string propertyName = null) {
PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (propertyName));
}
===========================
Edit #1
I don't really like that solution but I created public method in ConverterDisplay to create converter, it's being called after MainWindow is initialized so now the converters are correct.
Another thing is that how do i bind my UpdateDisplay method to InputValueProperty? I found through validation that it's getting correct value, but I can't see way how I can run that method without creating static stuff.
Concerning your second problem (binding the UpdateDisplay method to InputValueProperty: In general, it's not the best idea to call any method within a dependency property's setter, since this setter is never invoked when using data binding to fill the dependency property's value, as pointed out at MSDN:
The WPF XAML processor uses property system methods for dependency
properties when loading binary XAML and processing attributes that are
dependency properties. This effectively bypasses the property
wrappers. When you implement custom dependency properties, you must
account for this behavior and should avoid placing any other code in
your property wrapper other than the property system methods GetValue
and SetValue.
Instead, create a callback method that is invoked whenever InputValue's content changes, and call UpdateDisplay from there:
public static readonly DependencyProperty InputValueProperty = DependencyProperty.Register (
"InputValue",
typeof(int),
typeof(ConverterDisplay),
new PropertyMetadata (DEFAULT_INPUT_VALUE, InputValueChangedCallback));
private static void InputValueChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
{
var userControl = dependencyObject as ConverterDisplay;
if (userControl != null)
userControl.UpdateDisplay();
}
I have created a custom TextEditor control that inherits from AvalonEdit. I have done this to facilitate the use of MVVM and Caliburn Micro using this editor control. The [cut down for display purposes] MvvTextEditor class is
public class MvvmTextEditor : TextEditor, INotifyPropertyChanged
{
public MvvmTextEditor()
{
TextArea.SelectionChanged += TextArea_SelectionChanged;
}
void TextArea_SelectionChanged(object sender, EventArgs e)
{
this.SelectionStart = SelectionStart;
this.SelectionLength = SelectionLength;
}
public static readonly DependencyProperty SelectionLengthProperty =
DependencyProperty.Register("SelectionLength", typeof(int), typeof(MvvmTextEditor),
new PropertyMetadata((obj, args) =>
{
MvvmTextEditor target = (MvvmTextEditor)obj;
target.SelectionLength = (int)args.NewValue;
}));
public new int SelectionLength
{
get { return base.SelectionLength; }
set { SetValue(SelectionLengthProperty, value); }
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged([CallerMemberName] string caller = null)
{
var handler = PropertyChanged;
if (handler != null)
PropertyChanged(this, new PropertyChangedEventArgs(caller));
}
}
Now, in the view that holds this control, I have the following XAML:
<Controls:MvvmTextEditor
Caliburn:Message.Attach="[Event TextChanged] = [Action DocumentChanged()]"
TextLocation="{Binding TextLocation, Mode=TwoWay}"
SyntaxHighlighting="{Binding HighlightingDefinition}"
SelectionLength="{Binding SelectionLength,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
Document="{Binding Document, Mode=TwoWay}"/>
My issue is SelectionLength (and SelectionStart but let us just consider the length for now as the problem is the same). If I selected something with the mouse, the binding from the View to my View Model works great. Now, I have written a find and replace utility and I want to set the SelectionLength (which has get and set available in the TextEditor control) from the code behind. In my View Model I am simply setting SelectionLength = 50, I implement this in the View Model like
private int selectionLength;
public int SelectionLength
{
get { return selectionLength; }
set
{
if (selectionLength == value)
return;
selectionLength = value;
Console.WriteLine(String.Format("Selection Length = {0}", selectionLength));
NotifyOfPropertyChange(() => SelectionLength);
}
}
when I set SelectionLength = 50, the DependencyProperty SelectionLengthProperty does not get updated in the MvvmTextEditor class, it is like the TwoWay binding to my control is failing but using Snoop there is no sign of this. I thought this would just work via the binding, but this does not seem to be the case.
Is there something simple I am missing, or will I have to set up and event handler in the MvvmTextEditor class which listens for changes in my View Model and updated the DP itself [which presents it's own problems]?
Thanks for your time.
This is because the Getter and Setter from a DependencyProperty is only a .NET Wrapper. The Framework will use the GetValue and SetValue itself.
What you can try is to access the PropertyChangedCallback from your DependencyProperty and there set the correct Value.
public int SelectionLength
{
get { return (int)GetValue(SelectionLengthProperty); }
set { SetValue(SelectionLengthProperty, value); }
}
// Using a DependencyProperty as the backing store for SelectionLength. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectionLengthProperty =
DependencyProperty.Register("SelectionLength", typeof(int), typeof(MvvmTextEditor), new PropertyMetadata(0,SelectionLengthPropertyChanged));
private static void SelectionLengthPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var textEditor = obj as MvvmTextEditor;
textEditor.SelectionLength = e.NewValue;
}
Here is another answer if you are still open. Since SelectionLength is already defined as a dependency property on the base class, rather than create a derived class (or add an already existing property to the derived class), I would use an attached property to achieve the same functionality.
The key is to use System.ComponentModel.DependencyPropertyDescriptor to subscribe to the change event of the already existing SelectionLength dependency property and then take your desired action in the event handler.
Sample code below:
public class SomeBehavior
{
public static readonly DependencyProperty IsEnabledProperty
= DependencyProperty.RegisterAttached("IsEnabled",
typeof(bool), typeof(SomeBehavior), new PropertyMetadata(OnIsEnabledChanged));
public static void SetIsEnabled(DependencyObject dpo, bool value)
{
dpo.SetValue(IsEnabledProperty, value);
}
public static bool GetIsEnabled(DependencyObject dpo)
{
return (bool)dpo.GetValue(IsEnabledProperty);
}
private static void OnIsEnabledChanged(DependencyObject dpo, DependencyPropertyChangedEventArgs args)
{
var editor = dpo as TextEditor;
if (editor == null)
return;
var dpDescriptor = System.ComponentModel.DependencyPropertyDescriptor.FromProperty(TextEditor.SelectionLengthProperty,editor.GetType());
dpDescriptor.AddValueChanged(editor, OnSelectionLengthChanged);
}
private static void OnSelectionLengthChanged(object sender, EventArgs e)
{
var editor = (TextEditor)sender;
editor.Select(editor.SelectionStart, editor.SelectionLength);
}
}
Xaml below:
<Controls:TextEditor Behaviors:SomeBehavior.IsEnabled="True">
</Controls:TextEditor>
This is how I did this...
public static readonly DependencyProperty SelectionLengthProperty =
DependencyProperty.Register("SelectionLength", typeof(int), typeof(MvvmTextEditor),
new PropertyMetadata((obj, args) =>
{
MvvmTextEditor target = (MvvmTextEditor)obj;
if (target.SelectionLength != (int)args.NewValue)
{
target.SelectionLength = (int)args.NewValue;
target.Select(target.SelectionStart, (int)args.NewValue);
}
}));
public new int SelectionLength
{
get { return base.SelectionLength; }
//get { return (int)GetValue(SelectionLengthProperty); }
set { SetValue(SelectionLengthProperty, value); }
}
Sorry for any time wasted. I hope this helps someone else...
I've created a custom control that extends the RichTextBox so that I can create a binding for the xaml property. It all works well as long as I just update the property from the viewmodel but when I try to edit in the richtextbox the property is not updated back.
I have the following code in the extended version of the richtextbox.
public static readonly DependencyProperty TextProperty = DependencyProperty.Register ("Text", typeof(string), typeof(BindableRichTextBox), new PropertyMetadata(OnTextPropertyChanged));
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var rtb = d as BindableRichTextBox;
if (rtb == null)
return;
string xaml = null;
if (e.NewValue != null)
{
xaml = e.NewValue as string;
if (xaml == null)
return;
}
rtb.Xaml = xaml ?? string.Empty;
}
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
In the view I've set the binding like such
<Controls:BindableRichTextBox Text="{Binding XamlText, Mode=TwoWay}"/>
In the viewmodel I've created the XamlText as a normal property with the NotifyPropertyChanged event being called on updates.
I want the bound XamlText to be updated when the user enters texts in the RichTextBox either on lostfocus or directly during edit, it doesn't really matter.
How can I change the code to make this happen?
You will need to listen to changes to the Xaml-property of the BindableRichTextBox and set the Text-property accordingly. There is an answer available here describing how that could be achieved. Using the approach described in that would the result in the following code (untested):
public BindableRichTextBox()
{
this.RegisterForNotification("Xaml", this, (d,e) => ((BindableRichTextBox)d).Text = e.NewValue);
}
public void RegisterForNotification(string propertyName, FrameworkElement element, PropertyChangedCallback callback)
{
var binding = new Binding(propertyName) { Source = element };
var property = DependencyProperty.RegisterAttached(
"ListenAttached" + propertyName,
typeof(object),
typeof(UserControl),
new PropertyMetadata(callback));
element.SetBinding(property, binding);
}
I have the following WPF code. You can see in the comments there that I have a problem with my OnValueChanged handler. I need the code there to differentiate between a Value set from the UI (through various bindings) and one set from the manager class. I had hoped that DependencyPropertyChangedEventArgs would have some kind of source that I could use to differentiate this, but I don't see anything like that. Ideas? Is there some way to set a WPF DependencyProperty without triggering its PropertyChanged handler? Thanks for your time.
public class GaugeBaseControl : UserControl
{
protected readonly AssetModelManager Manager;
public GaugeBaseControl(AssetModelManager mgr)
{
Manager = mgr;
if(mgr != null)
mgr.TelemetryValueChanged += MgrOnTelemetryValueChanged; // coming on background thread
}
private void MgrOnTelemetryValueChanged(KeyValuePair<string, object> keyValuePair)
{
if(_localTelemetryId != keyValuePair.Key)
return;
Dispatcher.Invoke(DispatcherPriority.Normal, new Action(() =>
{
if (!Equals(Value, keyValuePair.Value))
Value = keyValuePair.Value;
}));
}
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var gbc = (GaugeBaseControl) d;
var id = gbc.TelemetryId;
if (!string.IsNullOrEmpty(id))
{
// this is the problem:
// I need to always set gbc.Manager[id] if this event was triggered from the UI (even when equal)
// however, if it was triggered by TelemetryValueChanged then we don't want to go around in circles
if (!Equals(gbc.Manager[id], e.NewValue))
gbc.Manager[id] = e.NewValue;
}
}
private string _localTelemetryId; // to save us a cross-thread check
private static void OnTelemetryIdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var gbc = (GaugeBaseControl)d;
var tid = gbc.TelemetryId;
gbc._localTelemetryId = tid;
gbc.Value = string.IsNullOrEmpty(tid) ? null : gbc.Manager[tid];
}
public static readonly DependencyProperty TelmetryIdProperty = DependencyProperty.Register("TelemetryId", typeof(string), typeof(GaugeBaseControl), new PropertyMetadata(OnTelemetryIdChanged));
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(object), typeof(GaugeBaseControl), new PropertyMetadata(OnValueChanged));
public object Value
{
get { return GetValue(ValueProperty); }
set { SetValue(ValueProperty, value);}
}
public string TelemetryId
{
get { return (string)GetValue(TelmetryIdProperty); }
set { SetValue(TelmetryIdProperty, value); }
}
}
It seems a bit hackish, but it is the best shot i could come up with without changing the architecture. You could stop listening on the TelemetryValueChanged event while doing your internal update to stop the roundtrip like so:
internal void SetManagerIdInternal(string id, object value)
{
if(mgr != null)
{
mgr.TelemetryValueChanged -= MgrOnTelemetryValueChanged;
mgr[id] = value;
mgr.TelemetryValueChanged += MgrOnTelemetryValueChanged;
}
}
And use it like this:
if (!Equals(gbc.Manager[id], e.NewValue))
SetManagerIdInternal(id, e.NewValue);
You could also use a private field to just skip doing work without unregistering/reregistering the event in MgrOnTelemetryValueChanged wich might be better performance wise, but i haven't tested it.