Bind an Action to a property of a UserControl in XAML
Asked Answered
P

1

6

I have a user control which has a button and a dependency property for the action the button is to execute. The page which contains the control sets the action in XAML.

MyUserControl.cs

A Button, and dependency property ButtonAction, of type Action. When the button is clicked it executes the ButtonAction.

MainPage.xaml.cs

Action Action1

Action Action2

MainPage.xaml

Present an instance of MyUserControl, with ButtonAction=Action1

The problem: The ButtonAction property is not assigned from the XAML

MyUserControl.cs

    public sealed partial class MyUserControl : UserControl
{

    public Action ButtonAction {
        get { return (Action)GetValue(ButtonActionProperty); }
        set { SetValue(ButtonActionProperty, value); }
    }

    public static readonly DependencyProperty ButtonActionProperty =
        DependencyProperty.Register("ButtonAction", typeof(Action), typeof(MyUserControl), new PropertyMetadata(null,ButtonAction_PropertyChanged));

    private static void ButtonAction_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
        Debug.WriteLine("ButtonAction_PropertyChanged");
        // Is not called!
    }


    public MyUserControl() {
        this.InitializeComponent();
    }

    private void Button_Click(object sender, RoutedEventArgs e) {
        if (ButtonAction != null) {
            // Never reaches here!
            ButtonAction();
        }
    }
}

MyUserControl.xaml

    <Grid>
    <Button Click="Button_Click">Do The Attached Action!</Button>

</Grid>

MainPage.xaml.cs

    Action Action1 = (
        () => { Debug.WriteLine("Action1 called"); });

    Action Action2 = (() => { Debug.WriteLine("Action2 called"); });

MainPage.xaml

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <local:MyUserControl x:Name="myUserControl" ButtonAction="{Binding Action1}"/>
</Grid>

It does work if in the code-behind for MainPage (MainPage.xaml.cs) I assign the action in the Loaded event.

        private void Page_Loaded(object sender, RoutedEventArgs e) {
        this.myUserControl.ButtonAction = Action1;
    }

In this case the PropertyChanged callback in the user control is also called. (This handler is provided only for diagnostic purposes. I can't see how it can be used to support the property in practice).

Panorama answered 25/2, 2015 at 5:43 Comment(2)
What's the DataContext from the MainPage? If its not the page then you have to set ButtonAction="{Binding Action1, ElementName=x:name of the MainPage}"Giron
The MainPage has no data context - but it works fine like that when the action is assigned in the code-behind. I tried your suggestion (set the name of the page, and ElementName) and it made no difference. But thanks anyway.Panorama
M
4

The issue is in your data binding. The Action1 in ButtonAction="{Binding Action1}" should be a public property while you defined it as a private variable.

Also, you cannot just declare a normal property directly in the code behind like that. You will need either a dependency property, or more commonly, a public property inside a viewmodel which implements INotifyPropertyChanged.

If we go with the second approach, we will need to create a viewmodel class like the following with an Action1 property. Note the OnPropertyChanged stuff is just the standard way of implementing INotifyPropertyChanged.

public class ViewModel : INotifyPropertyChanged
{
    private Action _action1;
    public Action Action1
    {
        get { return _action1; }
        set
        {
            _action1 = value;
            OnPropertyChanged("Action1");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string name)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }
}

And then you just need to assign this to the DataContext of your main page.

    public MainPage()
    {
        this.InitializeComponent();

        var vm = new ViewModel();
        vm.Action1 = (() =>
        {
            Debug.WriteLine("Action1 called");
        });

        this.DataContext = vm;
    }

With these two changes, your ButtonAction callback should be firing now. :)

Mohandas answered 4/3, 2015 at 5:5 Comment(10)
Both solutions confirmed to work. Very interesting that both INotifyPropertyChanged and DependencyObject work equivalently for data binding in xaml.Panorama
This may seem like common knowledge to WinRT programmers, and I think I already knew, but this question was unanswered, with a bonus, for about 5 days. I suggest that the trouble is that while we use these things in practice we really don't understand, particularly when it comes to the XAML.Panorama
The code for the dependency object solution is: 'class ViewModel : DependencyObject { public Action Action1 { get { return (Action)GetValue(Action1Property); } set { SetValue(Action1Property, value); } } public static readonly DependencyProperty Action1Property = DependencyProperty.Register("Action1", typeof(Action), typeof(ViewModel), new PropertyMetadata(null)); }`Panorama
That's correct. You might have already known that if you use the dp you will need to modify the xaml slightly by adding ElementName=MyMainPage to the binding expression. So something like ButtonAction="{Binding Action1, ElementName=MyMainPage}".Mohandas
I didn't need to add the element name with the dp solution. <local:MyUserControl ButtonAction="{Binding Action1}"/> works fine.Panorama
Where did you define your Action1 dependency property?Mohandas
I defined my Action1 dependency property in a new class, ViewModel : DependencyObject. The complete code is in my previous comment.Panorama
I see. I don't think dependency properties should be defined inside a vm. Also, a vm shouldn't implement DependencyObject. In the current context, a dp Action1 should be defined in MyMainPage.xaml.csMohandas
That's an improvement! Thanks! In the dp solution I've eliminated the class ViewModel, moved the Action1 dependency property into MainPage.xaml.cs, and changed XAML to ButtonAction="{Binding Action1, ElementName=MyMainPage}.Panorama
MainPage.xaml.cs is now: public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); Action1 = (() => { Debug.WriteLine("Action1 called"); }); } public Action Action1 { get { return (Action)GetValue(Action1Property); } set { SetValue(Action1Property, value); } } public static readonly DependencyProperty Action1Property = DependencyProperty.Register("Action1", typeof(Action), typeof(ViewModel), new PropertyMetadata(null)); }Panorama

© 2022 - 2024 — McMap. All rights reserved.