How to implement a WinUI 3 UserControl with ComboBox, Enum and DependencyProperty - c#

Suppose we have
public enum MyEnum {None, First, Second}
and in MainWindow.xaml.cs, we have
private IList<MyEnum> _myEnums = Enum.GetValues(typeof(MyEnum)).Cast<MyEnum>().ToList();
public IList<MyEnum> MyEnums => _myEnums;
public MyEnum SelectedMyEnum {get;set;}
and in MainWindow.xaml we have
<StackPanel>
<ComboBox ItemsSource="{x:Bind MyEnums}" SelectedItem="{x:Bind SelectedMyEnum, Mode=TwoWay}"/>
</StackPanel>
This works, as expected.
Now suppose we want to replace ComboBox with a user control, i.e.
<local:ExampleControl MyEnums="{x:Bind MyEnums}" SelectedMyEnum="{x:Bind SelectedMyEnum, Mode=TwoWay}"/>
So, in ExampleControl.xaml we have
<StackPanel>
<ComboBox
ItemsSource="{x:Bind MyEnums}"
SelectedItem="{x:Bind SelectedMyEnum,Mode=TwoWay}">
</ComboBox>
</StackPanel>
and in ExampleControl.xaml.cs we have
// SelectedMyEnum
public static readonly DependencyProperty SelectedMyEnumProperty =
DependencyProperty.Register(
"SelectedMyEnum",
typeof(MyEnum),
typeof(ExampleControl),
new PropertyMetadata(MyEnum.None)); // (1)
public MyEnum SelectedMyEnum
{
get { return (MyEnum)GetValue(SelectedMyEnumProperty); }
set { SetValue(SelectedMyEnumProperty, value); } // (2)
}
// MyEnums
public static readonly DependencyProperty MyEnumsProperty =
DependencyProperty.Register(
"MyEnums",
typeof(IEnumerable<MyEnum>),
typeof(ExampleControl), null
);
public IEnumerable<MyEnum> MyEnums
{
get { return (IEnumerable<MyEnum>)GetValue(MyEnumsProperty); }
set { SetValue(MyEnumsProperty, value); }
}
But this doesn't work. On startup, the SelectedMyEnum setter (2) is called repeatedly with the value MyEnum.None until there is a stack overflow,
System.StackOverflowException
HResult=0x800703E9
Source=<Cannot evaluate the exception source>
StackTrace:
<Cannot evaluate the exception stack trace>
Instead of new PropertyMetadata(MyEnum.None) in (1), I tried
new PropertyMetadata(MyEnum.None, OnEnumChanged)
with
private static void OnEnumChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var control = (ExampleControl)obj;
MyEnum myVal = (MyEnum)args.NewValue;
}
but that made no difference, the function OnEnumChanged was called repeatedly, along with the setter, with myVal == MyEnum.None, until stack overflow.
I checked the WinUI Gallery for examples of dependency properties for enums, but couldn't find any, but there were plenty of examples for double, int, and bool, e.g. ItemHeight in WrapPanel, I think I'm doing this right.
I must be missing something, but can't see it. I've searched on ComboBox, DependencyProperty, Enum, and found some matches, e.g Enum as a DependencyProperty of a UserControl, but I didn't find them helpful. Any help is appreciated.
Environment:
Microsoft Visual Studio Community 2022
Version 17.1.0
VisualStudio.17.Release/17.1.0+32210.238
Microsoft .NET Framework
Version 4.8.04161

Answered here by Roy Li - MSFT on the Microsoft Q&A site. It's related to a WinUI defect in the x:Bind markup extension. In the user control, in the case of enums, x:Bind doesn't work with SelectedMyEnum, it needs to be written using Binding as
<ComboBox
ItemsSource="{x:Bind MyEnums}"
SelectedItem="{Binding SelectedMyEnum,Mode=TwoWay}">
</ComboBox>
and DataContext set to this. In my experiments, it appears to be only an issue with x:Bind and TwoWay binding, OneWay and OneTime bindings with x:Bind appear okay. And not an issue if binding to strings rather than enums.

Related

Custom Control DataContext Doesn't Work

I have a Custom Controller that works fine like this:
<Controller:DLBox ID="{Binding SHVM.Selected.ID}" />
I mean binding ID to a property in ViewModel. But when I want to bind it like this:
<ScrollViewer DataContext="{Binding SHVM.Selected}">
<Controller:DLBox ID="{Binding ID}" />
</ScrollViewer>
Binding to DataContext of parent, It doesn't work at all. I have some other Custom Controllers and they do fine, But I don't know what the hell is this one's problem!
This is the controller:
public partial class DLBox : UserControl
{
public static DependencyProperty IDProperty =
DependencyProperty.Register("ID", typeof(int), typeof(DLBox),
new FrameworkPropertyMetadata(0,FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
(o, e) => (o as DLBox).IDPropertyChanged((int)e.NewValue)));
public int ID { get; set; }
private void IDPropertyChanged(int e)
{
ID = e;
}
}
Could someone please tell my what's wrong? Because I'm debugging for 6 hours straight and didn't find anything! Thanks a lot.
‌
‌
UPDATE:
That worked with just adding:
<... DataContext={Binding} .../>
And I don't know why!
Now the real problem is that I want to use this inside 2 ItemsControls and even with DataContext Still doesn't work.
(Just for clarification, I have 2 Lists inside each other. Think about the first one like 10 schools First List, and inside each school there is some studens Second List)
<ItemsControl ItemsSource="{Binding Source={StaticResource Locator}, Path=SHVM.Extra.Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl DataContext="{Binding}" ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Controller:DLBox DataContext="{Binding}" ID="{Binding ID}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
<ItemsControl.ItemTemplate>
</ItemsControl>
UPDATE 2:
ID is just a TextBlock In a UserControl. There is nothing that I can show here!
All I did, Just set the Text of TextBlock inside the PropertyCallBack (Didn't use MVVM inside my controllers):
<TextBlock x:Name="txtIDValue"/>
And inside CodeBehind:
private void IDPropertyChanged(string e)
{
ID= e;
txtIDValue.Text = e;
}
There is nothing relevant to this problem, And that's why I couldn't figure it out!
Appreciate any help.
ANSWER:
After 12 hours working on it, I find out that It was an idiotic mistake! I don't know why and when I set DataContext in my Controller's XAML!
Anyway, Thanks.
Your dependency property declaration is wrong, because the getter and setter of the CLR wrapper must call the GetValue and SetValue methods respectively. Besides that, your PropertyChangedCallback is redundant. There is no need to set the property again in a callback that is called when the property value has just been set.
The declaration should look like this:
public static readonly DependencyProperty IDProperty = DependencyProperty.Register(
"ID", typeof(int), typeof(DLBox),
new FrameworkPropertyMetadata(
0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public int ID
{
get { return (int)GetValue(IDProperty); }
set { SetValue(IDProperty, value); }
}

WPF ComboBox SelectedValue binding with null value shows up blank

I have a problem while trying to bind 2 or more Comboboxes SelectedValue to a property, that is null.
Only 1 of the comboboxes bound to this property will show the real value.
Below is my Xaml where i use DataTemplate to select a Combobox for presentation of the viewModel.
Xaml:
<Window x:Class="Test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Test"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate DataType="{x:Type local:PropertyValueViewModel}">
<ComboBox SelectedValue="{Binding Value}" ItemsSource="{Binding SelectableValues}" DisplayMemberPath="Description" SelectedValuePath="Value"/>
</DataTemplate>
</Window.Resources>
<StackPanel>
<Label Content="These uses template:"></Label>
<ContentPresenter Content="{Binding ValueSelector}"></ContentPresenter>
<ContentPresenter Content="{Binding ValueSelector}"></ContentPresenter>
<ContentPresenter Content="{Binding ValueSelector}"></ContentPresenter>
</StackPanel>
And the code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
ValueSelector = new PropertyValueViewModel()
{
SelectableValues = new List<SelectableValue>()
{
new SelectableValue("NULL", null),
new SelectableValue("1", 1)
},
Value = null
};
DataContext = this;
}
public static readonly DependencyProperty ValueSelectorProperty = DependencyProperty.Register(
"ValueSelector", typeof(PropertyValueViewModel), typeof(MainWindow), new PropertyMetadata(default(PropertyValueViewModel)));
public PropertyValueViewModel ValueSelector
{
get { return (PropertyValueViewModel)GetValue(ValueSelectorProperty); }
set { SetValue(ValueSelectorProperty, value); }
}
}
/// <summary>
/// My viewModel
/// </summary>
public class PropertyValueViewModel
{
public object Value { get; set; }
public object SelectableValues { get; set; }
}
/// <summary>
/// The items in the combobox
/// </summary>
public class SelectableValue
{
public SelectableValue(string header, object value)
{
Value = value;
Description = header;
}
public object Value { get; set; }
public string Description { get; set; }
}
Now i am wondering why only 1 of them can show the NULL value at startup?
I can change the value in any of them, and they will all sync with the value in the property - if i select 1 and then back to NULL, they will all show NULL.
It seems like its only the initial value is not shown correctly.
If i avoid using DataTemplate the binding works too.
Does anyone know why the DAtaTemplate behaves this way?
Interesting problem.
Fundamentally, this appears to be caused by your choice to use null as one of the selectable values. null, of course, has special meaning, for C#, .NET, and WPF. The problem also involves the order in which the initialization of the ComboBox element is done. The SelectedValuePath property is initialized after the SelectedValue property.
This means that as your program is starting up and the ComboBox elements are created, when null is assigned to the SelectedValue property through its binding, the ComboBox does not yet have enough information to handle that value as a legitimate item selection. Instead, it interprets it as no selection at all.
Why does the last ComboBox still get initialized the way you want? I'm not really sure…I didn't investigate very far regarding that. I could speculate, but the odds of my guessing correctly seem low so I won't bother. Since it's the anomaly and not necessarily in keeping with expected behavior (based on above, even if the behavior is the desired behavior) I'll chalk it up to one of WPF's many "quirks". :)
I found several work-arounds for the issue:
Don't use null as a selectable value. If every selectable value is non-null, then the non-null value used to initialize each element's SelectedValue property is retained and when the SelectedValuePath is initialized, the current selection for the ComboBox is set correctly.
Don't use SelectedValuePath. Instead, just bind to SelectedItem and initialize the Value property with the desired SelectableValue class instance (e.g. the first one in the list).
In the ComboBox's Loaded event, refresh the target of the binding.
The first two are significant departures from your current design. Personally, if at all possible I would go with one or the other. It seems to me that there's a clear danger in using null as a selectable value in a ComboBox, and this may not be the only oddity you run into. In the long run, maintenance of this part of the code may cost a lot more if you continue to use null.
That said, the third option does work, and if you're lucky, the only real hazard in using null is on initialization. My proposed work-around for that option would look something like this:
XAML:
<DataTemplate DataType="{x:Type local:PropertyValueViewModel}">
<ComboBox SelectedValue="{Binding Value}"
ItemsSource="{Binding SelectableValues}"
DisplayMemberPath="Description"
SelectedValuePath="Value"
Loaded="comboBox_Loaded"/>
</DataTemplate>
C#:
private void comboBox_Loaded(object sender, RoutedEventArgs e)
{
ComboBox comboBox = (ComboBox)e.OriginalSource;
BindingOperations.GetBindingExpression(comboBox, ComboBox.SelectedValueProperty)
.UpdateTarget();
}
This forces WPF to update the target (i.e. the SelectedValue property of the control). Since at this point, the SelectedValuePath has been set, assigning null to the property this time correctly updates the selected item for the ComboBox.
By the way, I would strongly recommend that you disambiguate the names of the Value properties in your models. Having two different Value properties used for bindings in a single XAML element is very confusing. I would use, for example, SelectedValue and ItemValue, for the PropertyValueViewModel class and the SelectableValue class, respectively.

Why is this basic binding failing with an exception?

Can anyone explain why the binding on TagObject below code throws the following binding exception?
System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=Value; DataItem=null; target element is 'TagObject' (HashCode=37895910); target property is 'Value' (type 'String')
My suspicion is its because TagObject itself isn't a subclass of FrameworkElement so it doesn't have a data context itself and thus doesn't know how to resolve the XAML binding.
To test, I changed the base type of TagObject to FrameworkElement and sure enough, the binding error went away, but Value still didn't change. My theory there is although the binding was now valid, TagObject wasn't part of the Visual Tree, therefore it didn't inherit its DataContext.
I also tried giving 'TextBlocka name, then specifying it as theElementNamein the binding, but that again threw a binding exception. In this case, my suspicion is that it can't find the named element becauseTagObject` still is not part of the visual tree, even with the base-class change above.
For the record, I do know a solution would be to simply hide that object creation behind a ValueConverter to wrap it for me, but I'm wondering if there's a XAML-only solution to address that binding on TagObject.
Here's the XAML:
<Window x:Class="Test.TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:test="clr-namespace:Test">
<Window.Resources>
<DataTemplate DataType="{x:Type test:DataObject}">
<TextBlock Text="{Binding Value}">
<TextBlock.Tag>
<test:TagObject Value="{Binding Value}" />
</TextBlock.Tag>
</TextBlock>
</DataTemplate>
</Window.Resources>
<ListBox x:Name="MainListBox" BorderThickness="0" />
</Window>
This doesn't work either...
<TextBlock x:Name="MyTextBlock" Text="Test">
<TextBlock.Tag>
<test:TargetObject Value="{Binding DataContext.Value, ElementName=MyTextBlock}" />
</TextBlock.Tag>
</TextBlock>
Here's the code:
using System.Windows;
using System.Collections.ObjectModel;
namespace Test
{
public partial class TestWindow : Window
{
public TestWindow()
{
InitializeComponent();
var sourceItems = new ObservableCollection<DataObject>();
for(int i = 1; i <= 10; i++)
sourceItems.Add(new DataObject() { Value = "Item " + i});
MainListBox.ItemsSource = sourceItems;
}
}
public class DataObject : DependencyObject
{
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
"Value",
typeof(string),
typeof(DataObject),
new UIPropertyMetadata(null));
public string Value
{
get { return (string)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
}
public class TagObject : DependencyObject
{
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
"Value",
typeof(string),
typeof(TagObject),
new UIPropertyMetadata(null));
public string Value
{
get { return (string)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
}
}
I recreated your sample-code in a VS2013 solution and replicated what you're seeing. The binding seems to be happening just fine, but yes - it is spitting out those annoying error messages. After some research, it appears that this is a known bug -- I see others complaining of it as well. The binding is working fine. The problem, as far as I can tell from others, is that, for ItemsSources, WPF is trying to optimize the evaluation of the styles and data-templates as it composes the visual tree, sometimes doing this before the binding is available on the actual elements. In your case, the TextBlock is already available, and it has a binding value, but the Tag property within it is composed before that and thus complains of a missing FrameworkElement (which, an instant later, is no longer missing).
I do not find this to be an encouraging sign from the WPF team, as this seems like a very simple scenario. Correct code should never be emitting warnings or errors.

Binding to attached property

I'm trying to bind from Button's ContentTemplate to attached property. I read all the answers for question similar to "binding to attached property" but I had no luck resolving the problem.
Please note that example presented here is a dumbed down version of my problem to avoid cluttering the problem with business code.
So, I do have static class with attached property:
using System.Windows;
namespace AttachedPropertyTest
{
public static class Extender
{
public static readonly DependencyProperty AttachedTextProperty =
DependencyProperty.RegisterAttached(
"AttachedText",
typeof(string),
typeof(DependencyObject),
new PropertyMetadata(string.Empty));
public static void SetAttachedText(DependencyObject obj, string value)
{
obj.SetValue(AttachedTextProperty, value);
}
public static string GetAttachedText(DependencyObject obj)
{
return (string)obj.GetValue(AttachedTextProperty);
}
}
}
and a window:
<Window x:Class="AttachedPropertyTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:AttachedPropertyTest"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Button local:Extender.AttachedText="Attached">
<TextBlock
Text="{Binding
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Button},
Path=(local:Extender.AttachedText)}"/>
</Button>
</Grid>
</Window>
That's pretty much it. I would expect t see "Attached" in the middle of the button.
Instead it crashes with: Property path is not valid. 'Extender' does not have a public property named 'AttachedText'.
I've set breakpoints on SetAttachedText and GetAttachedText, and SetAttachedText is executed, so attaching it to button works. GetAttachedText is never executed though, so it does not find property when resolving.
My problem is actually more complicated (I'm trying to do binding from inside of Style in App.xaml) but let's start with the simple one.
Did I miss something?
Thanks,
Your attached property registration is wrong. The ownerType is Extender, not DependencyObject.
public static readonly DependencyProperty AttachedTextProperty =
DependencyProperty.RegisterAttached(
"AttachedText",
typeof(string),
typeof(Extender), // here
new PropertyMetadata(string.Empty));
See the MSDN documentation for RegisterAttached:
ownerType - The owner type that is registering the dependency property

ValidationRules not removing error when value is set back to valid value

INTRODUCTION
I have created a DecimalTextBox UserControl that houses some decimal validation I need done, so that I dont need to recreate the validation each time, and can just use the UserControl instead. This validation has properties that need to be bound to, and so I have created DependencyProperties so I can bind to them, according to this article by Josh Smith.
THE PROBLEM
The control's validation is behaving strangely. When I type an erroneous value into the TextBox, it shows up as an error. However when I try to change the value back in the code the value shown in the textbox remains unchanged.
Here are the steps I perform that cause this error (in this example 1 is an invalid value):
Load the form and the default value is 0.
Enter 1 into the textbox (and the textbox goes red due to the validation result being and error)
In the code I set the property bound to the textbox to 0
The form still displays 1 in a red textbox
CODE EXAMPLE
I prepaired an example demonstrating the problem, which can be downloaded here.
I'll post some of the code here, if you want more let me know.
ValidationTestControl's XAML
<UserControl x:Class="WPFTestProject.ValidationTestControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:v="clr-namespace:WPFTestProject"
x:Name="ValidationTest"
Height="50" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="Type 'Banana' here: "></TextBlock>
<TextBox MinWidth="100">
<TextBox.Text>
<Binding ElementName="ValidationTest" Path="Text" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay" ValidatesOnDataErrors="True" ValidatesOnExceptions="True">
<Binding.ValidationRules>
<v:NotBananaValidationRule>
<v:NotBananaValidationRule.NotWhatBinding>
<v:NotBananaBinding x:Name="NotBananaValidationBinding"></v:NotBananaBinding>
</v:NotBananaValidationRule.NotWhatBinding>
</v:NotBananaValidationRule>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBlock Text=" (the text will give error when = 'Banana')"></TextBlock>
</StackPanel>
</Grid>
ValidationTestControls Code Behind
(yes I know not very MVVM but I felt it was ok for this stand alone control)
public partial class ValidationTestControl : UserControl
{
public ValidationTestControl()
{
InitializeComponent();
Banana = "Banana";
Binding BananaBinding = new Binding("Banana");
BananaBinding.Source = this;
NotBananaValidationBinding.SetBinding(NotBananaBinding.NotWhatProperty, BananaBinding);
}
public static DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(ValidationTestControl), new PropertyMetadata());
public static DependencyProperty BananaProperty = DependencyProperty.Register("Banana", typeof(string), typeof(ValidationTestControl), new PropertyMetadata());
public string Text
{
get
{
return (string)GetValue(TextProperty);
}
set
{
SetValue(TextProperty, value);
}
}
public string Banana
{
get
{
return (string)GetValue(BananaProperty);
}
set
{
SetValue(BananaProperty, value);
}
}
}
ValidationRule and FrameWorkElement created for binding
public class NotBananaValidationRule:ValidationRule
{
private NotBananaBinding _notWhatBinding;
public NotBananaBinding NotWhatBinding
{
get { return _notWhatBinding; }
set { _notWhatBinding = value; }
}
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
string what = value.ToString();
if(what == _notWhatBinding.NotWhat||string.IsNullOrEmpty(what))
return new ValidationResult(false,
"Please enter a string that is not " + _notWhatBinding.NotWhat);
else
return new ValidationResult(true, null);
}
}
public class NotBananaBinding : FrameworkElement
{
public static readonly DependencyProperty NotWhatProperty = DependencyProperty.Register(
"NotWhat", typeof(string), typeof(NotBananaBinding), new UIPropertyMetadata());
public string NotWhat
{
get { return (string)GetValue(NotWhatProperty); }
set { SetValue(NotWhatProperty, value); }
}
public NotBananaBinding() { }
}
Basically what this code does is check if you have typed "Banana" and then returns a validation error. The control exposes dependency properties because I want to be able to bind to them when I use the control. The FrameworkElement NotBananaBinding lets me create dependency properties (because it is a DependencyObject so i can bind stuff for the validation. The ValidationRule has a NotBananaBinding property that stores the dependency property and uses it in the validate method.
I know my property names are kinda crappy, sorry. The thing is that the example does a good job of displaying the error. In my haste to make an example I didn't name the variables very well. If you find the code crappy please download the sample here.
WHAT I'VE FIGURED OUT SO FAR
Basically this problem seems to be caused by the fact that I am not actually changing the value.
Even if I call OnPropertyChanged on the property, because the value is not different it doesn't try and reevaluate the Validation.
I can obviously change the value to some arbitrary Valid value and then change it to the one I want it to be and it will work, but I was hoping there is some way to get call validation manually, to reevaluate the value and then change it etc. The changing it away and back is kinda messy.
CONCLUSION
Am I doing something wrong (perhaps something about the way I implemented the validation and binding from Josh Smiths post)
Is this just a c# bug, or is the behavior intended? If so then why?
Are there any elegant ways to fix it?
u_u
The validation prevents the Text property to be set. Put a break point on the setter and you will see that it will not break when you type the last 'a'. If you type Bananan and hit Backspace and it errors, push the button and it will work. The validation makes sure that there can be no invalid value in your property. So if you save it to let's say a database while in error, it won't save an invalid value.

Categories