Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can you access XAML-set properties of a UserControl during initialization?

We're writing a custom UserControl (as opposed to a lookless control) and we need to perform some initialization based on what properties our consumers set on our control in XAML.

Now in most cases, you'd use the Initialized event (or the OnInitialized override) since by the time that fires, all the XAML-set properties have been applied, but in the case of a UserControl, that isn't the case. When the Initialized event fires, all properties are still at their default values.

I didn't notice this for other controls, just UserControls, which are different in that they call InitializeComponent() in their constructor, so as a test, I commented that line out and ran the code and sure enough, this time during the Initialized event, the properties were set.

Here is some code and test results demonstrating this...

Result with InitializeComponent called in the constructor:
(Note: The values still have not been set)

TestValue (Pre-OnInitialized): Original Value
TestValue (Initialized Event): Original Value
TestValue (Post-OnInitialized): Original Value

Result with InitializeComponent completely commented out:
(Note: While the values have been set, the control isn't loaded as it needs InitializeComponent)

TestValue (Pre-OnInitialized): New Value!
TestValue (Initialized Event): New Value!
TestValue (Post-OnInitialized): New Value! // Event *was* called 

and the property has been changed

All this said, what can I use to initialize my control based on user-set properties in the XAML? (Note: Loaded is too late as the control should already have been initialized by then.)

XAML Snippet

<local:TestControl TestValue="New Value!" />

TestControl.cs

public partial class TestControl : UserControl {

    public TestControl() {
        this.Initialized += TestControl_Initialized;
        InitializeComponent();
    }

    protected override void OnInitialized(EventArgs e) {
        Console.WriteLine("TestValue (Pre-OnInitialized): " + TestValue);
        base.OnInitialized(e);
        Console.WriteLine("TestValue (Post-OnInitialized): " + TestValue);
    }

    void TestControl_Initialized(object sender, EventArgs e) {
        Console.WriteLine("TestValue (Initialized Event): " + TestValue);
    }

    public static readonly DependencyProperty TestValueProperty = DependencyProperty.Register(
        nameof(TestValue),
        typeof(string),
        typeof(TestControl),
        new UIPropertyMetadata("Original Value"));

    public string TestValue {
        get => (string)GetValue(TestValueProperty);
        set => SetValue(TestValueProperty, value);
    }
}
like image 467
Mark A. Donohoe Avatar asked Sep 03 '13 02:09

Mark A. Donohoe


1 Answers

Awesomesausage! I figured it out!

Normally when you receive the Initialized event (or are inside the OnInitialized override) you have access to XAML-set property values. However, UserControl classes work a little differently as they depend on InitializeComponent being called to hydrate the UI and set the related member variables, etc.

The problem is that call is in the constructor, which in turn ends up calling OnInitialized (and thus raising the Initialized event) but that happens way before the XAML-set properties have been applied, meaning you don’t have access to them yet, which I needed.

One may think that's a good use for the Loaded event--to finish initialization based on those properties--but if you're performing additional initialization there, you're creating a potential race condition with your consumers in that if they subscribe to your Loaded event and get it before you, then in their handler try to access your control, they will be accessing an uninitialized control.

Then something occurred to me... As I showed above, if you remove the InitializeComponent call from the constructor, the Initialized event now works as you would expect, but of course your UI isn't hydrated yet since you haven't yet called InitializeComponent.

So what would happen if you moved that call to the beginning of the OnInitialized override, before the call to base.OnInitialized, and thus before the Initialized event was raised?

Yep! That worked! :)

This way not only do you have the XAML-set properties, but you’d also have the UI fully loaded before anyone gets the Initialized event (let alone the Loaded event), which is how the Initialized event is supposed to be used.

Below is the revised code...

public partial class TestControl : UserControl {

    protected override void OnInitialized(EventArgs e) {
        InitializeComponent();
        base.OnInitialized(e);
    }

    public static readonly DependencyProperty TestValueProperty = DependencyProperty.Register(
        nameof(TestValue),
        typeof(string),
        typeof(TestControl),
        new UIPropertyMetadata("Original Value"));

    public string TestValue {
        get => (string)GetValue(TestValueProperty);
        set => SetValue(TestValueProperty, value);
    }
}
  • Note: You don't need the constructor anymore unless you have a specific need to do other things there. And if you do, just remember you can't access constituent controls by name until after the InitializeComponent call, but that just means you have to plan to move such name-based initialization between InitializeComponent and that call to base.OnInitialize and things will work just fine.
like image 94
Mark A. Donohoe Avatar answered Nov 11 '22 21:11

Mark A. Donohoe