1
votes

I am building a user control for a TextBox because I want it to have some special behaviour.

The control can be used in several contexts, including as a flyout for a button. When it is a flyout I want to close the flyout when the user presses the Enter key while editing text.

Textbox is presented as flyout, and user presses enter to close the flyout]![enter image description here

To achieve this, the control has a ParentButton dependency property which, if set, stores the button with the flyout, and the XAML for the parent page sets it in this case. The control has a KeyUp handler which detects the Enter key and, if ParentButton property is set, closes its flyout.

TextBoxUC.xaml

<UserControl
x:Class="TextBoxUCDemo.TextBoxUC"
...
xmlns:local="using:TextBoxUCDemo"
...>

<StackPanel Width="250">
    <TextBox KeyUp="TextBox_KeyUp" Text="Hello" />
</StackPanel>

TextBoxUC.xaml.cs

    public sealed partial class TextBoxUC : UserControl
{
    public TextBoxUC() {
        this.InitializeComponent();
    }

    internal static readonly DependencyProperty ParentButtonProperty =
      DependencyProperty.Register("ParentButton", typeof(Button), typeof(TextBoxUC), new PropertyMetadata(null));

    public Button ParentButton {
        get { return ((Button)GetValue(ParentButtonProperty)); }
        set { SetValue(ParentButtonProperty, value); }
    }

    private void TextBox_KeyUp(object sender, KeyRoutedEventArgs e) {

        switch (e.Key) {
            case VirtualKey.Enter:

                // (Do something with the Text...)

                // If this is a flyout from a button then hide the flyout.
                if (ParentButton != null) { // Always null!
                    ParentButton.Flyout.Hide();
                }

                break;
            default: return;
        }

    }

}

MainPage.xaml

<Page
x:Class="TextBoxUCDemo.MainPage"
...
xmlns:local="using:TextBoxUCDemo"
...>

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Margin="200,300">
    <Button Name="flyoutTextBoxButton" Content="Edit">
        <Button.Flyout>
            <Flyout>
                <local:TextBoxUC ParentButton="{Binding ElementName=flyoutTextBoxButton, Path=.}"/>
            </Flyout>
        </Button.Flyout>
    </Button>

</Grid>

The problem is that the ParentButton is always null.

-- Edit --

I've narrowed the problem down to the binding to the element in the XAML. If I set the ParentButton from the code-behind of the MainPage, then it works.

In 'MainPage.xaml':

Loaded="Page_Loaded"
....
<local:TextBoxUC/>

In MainPage.xaml.cs

private void Page_Loaded(object sender, RoutedEventArgs e) {
    textBoxUC.ParentButton = this.flyoutTextBoxButton;
}

Effect:

                    if (ParentButton != null) { 
                       // Reaches here
                }

So: THE PROBLEM is in the xaml ParentButton="{Binding ElementName=flyoutTextBoxButton, Path=.}", which compiles but has no effect.

If I add a changed event handler to the registration of the dependency property, then the handler is called when the ParentButton is set from the code-behind, but never called for the binding to the ElementName. The handler seems to be only useful for debugging purposes. I can't see that it is needed to make the property work.

3
Both of the solutions below have a workaround which is a signficant improvement on the original architecture. They allow me to close the flyout without the UserControl referring to it. The problem of binding the xaml element is interesting, but should be the subject of a separate question with a simpler demonstration. I have accepted the solution which uses a Behaviour (Jerry Nixon). Thanks again - it's a pity I can't accept both. – - Stephen Hosking

3 Answers

7
votes

Okay, how about this? I've used it in the past. Works fine.

[Microsoft.Xaml.Interactivity.TypeConstraint(typeof(Windows.UI.Xaml.Controls.TextBox))]
public class CloseFlyoutOnEnterBehavior : DependencyObject, IBehavior
{
    public DependencyObject AssociatedObject { get; set; }

    public void Attach(DependencyObject obj)
    {
        this.AssociatedObject = obj;
        (obj as TextBox).KeyUp += TextBox_KeyUp;
    }

    void TextBox_KeyUp(object sender, KeyRoutedEventArgs e)

    {
        if (!e.Key.Equals(Windows.System.VirtualKey.Enter))
            return;
        var parent = this.AssociatedObject;
        while (parent != null)
        {
            if (parent is FlyoutPresenter)
            {
                ((parent as FlyoutPresenter).Parent as Popup).IsOpen = false;
                return;
            }
            else
            {
                parent = VisualTreeHelper.GetParent(parent);
            }
        }
    }

    public void Detach()
    {
        (this.AssociatedObject as TextBox).KeyUp -= TextBox_KeyUp;
    }
}

Use it like this:

<Button HorizontalAlignment="Center"
        VerticalAlignment="Center"
        Content="Click Me">
    <Button.Flyout>
        <Flyout Placement="Bottom">
            <TextBox Width="200"
                        Header="Name"
                        PlaceholderText="Jerry Nixon">
                <Interactivity:Interaction.Behaviors>
                    <local:CloseFlyoutOnEnterBehavior />
                </Interactivity:Interaction.Behaviors>
            </TextBox>
        </Flyout>
    </Button.Flyout>
</Button>

Learn more about behaviors here:

http://blog.jerrynixon.com/2013/10/everything-i-know-about-behaviors-in.html

And here (lesson 3):

http://blog.jerrynixon.com/2014/01/the-most-comprehensive-blend-for-visual.html

Best of luck!

1
votes

You can add to your control normal property of type Action that will contain lambda expression. You will set this property when creating control and then invoke it inside your control on EnterPressed event.

public class MyControll
{
  public Action ActionAfterEnterPressed {get; set;}

  private void HandleOnEnterPressed()
  {
   if(ActionAfterEnterPressed != null)
   {
     ActionAfterEnterPressed.Invoke();
   }
  }
}

somwhere where you create your control

...
MyControl c = new MyControl()
c.ActionAfterEnterPressed = CloseFlyuot;
....
private void CloseFlyuot()
{
  _myFlyout.IsOpen = false;
}

This way you can set any action and invoke it when needed from inside of your control withou needing to bother with what action actually does. Best of luck.

0
votes

You're making it a dependency property. That's the first, right start. But until you handle the changed event, you aren't really going to get any value from it.

I discuss this more here:

http://blog.jerrynixon.com/2013/07/solved-two-way-binding-inside-user.html

Best of luck!