1

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.

Stephen Hosking
  • 1,405
  • 16
  • 34
  • 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 Feb 18 '15 at 01:02

3 Answers3

7

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!

Jerry Nixon
  • 31,313
  • 14
  • 117
  • 233
  • Confirmed to also work. That is a very helpful alternative approach and I'm glad to be introduced to behaviours. – Stephen Hosking Feb 17 '15 at 02:39
  • To use this code in my example, replace the `TextBox` in the xaml (above) with `TextBoxUC`. Also need to add an assembly reference to `Behaviours SDK (XAML). Otherwise, it works 'as is'. – Stephen Hosking Feb 17 '15 at 02:45
  • 1
    One thing I like about this solution is that it is a clean, encapsulated solution that can work on any type of control without requiring it to be modified. It's also extensible so that you won't close the Flyout if, for example, the TextBox is empty. Again, without any customization to the control this is declared on. This is also a stateful solution which means stack enumerables are not required to solve it, like if we tried to do the same with an attached property. – Jerry Nixon Feb 17 '15 at 23:26
1

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.

Stephen Hosking
  • 1,405
  • 16
  • 34
  • This is an improved architecture, and works. To initialise the control, instead of your second block of code I added a Page_Loaded event handler to MainPage.xaml.cs, with this code `private void Page_Loaded(object sender, RoutedEventArgs e) { this.textBoxUC.ActionAfterEnterPressed = (() => this.flyoutTextBoxButton.Flyout.Hide()); }` – Stephen Hosking Feb 17 '15 at 02:41
0

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!

Jerry Nixon
  • 31,313
  • 14
  • 117
  • 233
  • I followed the suggestion in two ways: 1. Add a `PropertyMetadata` with an event handler in the registration of the dependency property. The event handler is never called. 2. Add the reusable `SetValueDp` method and call it in the `ParentButton` setter, as follows: `set { SetValueDp(ParentButtonProperty, value); }`, but the setter is never called. I'm sure the main problem is in the XAML. The code behind may need a changed event handler, but that is not clear yet. I've edited the question accordingly. – Stephen Hosking Feb 15 '15 at 23:37
  • Adding the changed event handler was helpful for debugging, as explained in my edit. – Stephen Hosking Feb 15 '15 at 23:47