3

I want to update my Popup's position when the size of its parent is changing.
The following code works but there's a problem.
As you can see, inside the popup there's a big button (width 300), before the textbox reach this size, it don't update the popup position (try it yourself - write a super big sentence and you will see)

<TabControl>
    <TabControl.ItemContainerStyle>
        <Style TargetType="{x:Type TabItem}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TabItem}">
                        <Grid Height="26" 
                              Background="{TemplateBinding Background}" 
                              x:Name="TabGrid">
                            <Grid.ColumnDefinitions>
                                 <ColumnDefinition Width="Auto" />
                                 <ColumnDefinition Width="Auto" />
                             </Grid.ColumnDefinitions>
                             <ContentPresenter x:Name="tabTitle" Margin="5,0" 
                                               HorizontalAlignment="Left" 
                                               VerticalAlignment="Center" 
                                               ContentSource="Header"/>
                            <StackPanel Grid.Column="1" Height="26" Margin="0,0,1,0" 
                                        HorizontalAlignment="Center" 
                                        VerticalAlignment="Center" 
                                        Orientation="Horizontal">
                                <ToggleButton x:Name="Edit" Width="16" Content="e" 
                                              ToolTip="Edit" />
                                <Popup AllowsTransparency="True" 
                                       IsOpen="{Binding IsChecked, ElementName=Edit}" 
                                        Placement="Right" 
                                        PlacementTarget="{Binding ElementName=TabGrid}"
                                        StaysOpen="False" 
                                        VerticalOffset="30" 
                                        HorizontalOffset="-20">
                                    <Grid x:Name="PopupGrid">
                                        <Grid.RowDefinitions>
                                            <RowDefinition Height="Auto" />
                                             <RowDefinition Height="*" />
                                         </Grid.RowDefinitions>
                                         <Border Width="16" Height="3" Margin="0,0,20,0" 
                                                 HorizontalAlignment="Right" 
                                                 Panel.ZIndex="1" Background="White" />
                                        <Border Grid.Row="1" Margin="0,-2,0,0" 
                                                Background="White" 
                                                BorderBrush="{Binding TabColor}" 
                                                BorderThickness="2">
                                                <StackPanel>
                                                <TextBox Name="Text" 
                                                         Text="{Binding Content, ElementName=tabTitle, UpdateSourceTrigger=PropertyChanged}" 
                                                         Margin="10"/>
                                                <Button Width="300"/>
                                            </StackPanel>
                                         </Border>
                                     </Grid>
                                </Popup>
                             </StackPanel>
                        </Grid>
                     </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
     </TabControl.ItemContainerStyle>
    <TabItem Header="TabItem">
        <Grid Background="#FFE5E5E5"/>
    </TabItem>
    <TabItem Header="TabItem">
        <Grid Background="#FFE5E5E5"/>
    </TabItem>
</TabControl>
Walt Ritscher
  • 6,977
  • 1
  • 28
  • 35
Ron
  • 3,975
  • 17
  • 80
  • 130
  • PopUp rearrange itself on parent size change. Only place where it doesn't work when parent window gets re-sized. So, you want it to re-sized in case parent window changes as well? – Rohit Vats Oct 03 '14 at 11:09
  • Can you show how placement button changes its position? – Rohit Vats Oct 03 '14 at 11:13
  • @RohitVats the popup does not reposition itself if the parent size change.. that'ss exactly the problem. I want the popup to be on the left bottom cornet ALWAYS (even when parent size changes) and it doesnt work – Ron Oct 03 '14 at 13:00
  • ToggleButton is always of same size. It's just it is displaced from its position a bit. However, I tried to replicate this in small sample and it works fine for me. Can you post small sample here replicating this problem? – Rohit Vats Oct 03 '14 at 13:03
  • On side note, width is constant i.e. 16. So why do you need converter to calculate the value. Simply set `HorizontalOffset` to 36. – Rohit Vats Oct 03 '14 at 13:05
  • @RohitVats That's true. the code above is outdated. I will update it – Ron Oct 03 '14 at 13:08
  • @RohitVats, I've updated the code and description above I also found the accurate problem – Ron Oct 03 '14 at 13:43
  • Is it necessary to use a `Popup`? If you can get away with using an `Adorner` instead, keeping the adorner positioned relative to the adorned element is more straightforward. – Mike Strobel Oct 06 '14 at 15:26
  • @MikeStrobel I dont mind to use anything else as long as it act as I want.. visible/hidden binded to toggle button, positioned relative and if it's open and user clicked on something else, it will close (just like regular popup) – Ron Oct 06 '14 at 16:54
  • @Ron Adorners meet all of those requirements. The only significant difference is that an adorner is clipped to the bounds of the parent window, i.e., it cannot extend beyond the window bounds the way a popup can. I'm not sure if that's an issue for you or not. – Mike Strobel Oct 06 '14 at 17:13
  • @MikeStrobel It shouldnt be a problem. do you have any example or something I can use as a start? – Ron Oct 06 '14 at 17:14

2 Answers2

1

Issue is PopUp needs some notification to update itself. So, as a workaround you can follow these steps to get it updated.

  1. Hook TextChanged event for TextBox where you can raise some notification to popUp to update itself.

  2. Next challenge would be how to get to popUp instance from TextBox. For that we can store the popUp in Tag of TextBox which we can access from code.

  3. One of the property which results in re-calculation of popUp placement is VerticalOffset which we can set manually to force popUp to recalculate position.


That being said here is the code (updated XAML code):

<Popup x:Name="popUp" AllowsTransparency="True"
       IsOpen="{Binding IsChecked, ElementName=Edit}" Placement="Right"
       PlacementTarget="{Binding ElementName=TabGrid}" StaysOpen="False"
       VerticalOffset="30" HorizontalOffset="-20">
    <Grid x:Name="PopupGrid">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Border Width="16" Height="3" Margin="0,0,20,0" HorizontalAlignment="Right" 
                Panel.ZIndex="1" Background="White" />
        <Border Grid.Row="1" Margin="0,-2,0,0" Background="White" BorderBrush="Black" 
                BorderThickness="2">
            <StackPanel>
                <TextBox Name="Text" TextChanged="Text_TextChanged"
                         Tag="{Binding ElementName=popUp}"
                         Text="{Binding Content, ElementName=tabTitle, 
                                  UpdateSourceTrigger=PropertyChanged}" Margin="10"/>
                <Button Width="300"/>
            </StackPanel>
        </Border>
    </Grid>
</Popup>

Code behind:

private void Text_TextChanged(object sender, TextChangedEventArgs e)
{
    Popup popup = ((TextBox)sender).Tag as Popup;
    if (popup != null)
    {
        popup.VerticalOffset += 1;
        popup.VerticalOffset -= 1;
    }
}
Rohit Vats
  • 79,502
  • 12
  • 161
  • 185
  • x:Reference is not available in .NET 3.5 – Ron Oct 03 '14 at 14:06
  • Use `Tag="{Binding ElementName=popUp}"`. Update in answer as well. – Rohit Vats Oct 03 '14 at 14:12
  • Ok, it does work not but there are 2 other problems it caused.. 1. The popup position is not updated on the first letter I write 2. It is not as smooth as it should be – Ron Oct 03 '14 at 14:15
  • 1
    I don't know for sure, but I think if you do `popup.HorizontalOffset += 1; popup.HorizontalOffset -= 1;`, the animation is slightly smoother. It still has problem 1 though. –  Oct 07 '14 at 22:31
0

I created a behavior for your Popup, using the reflection solution from this question. I'm not sure whether that is a right solution to you... I tested the implementation on .NET3.5 without any issues.

Add the PopupBehavior class to your project:

/// <summary>
/// Attaches alignment behavior to a Popup element.
/// </summary>
public class PopupBehavior : Behavior<Popup>
{
    #region Public fields

    public static readonly DependencyProperty HeaderWidthProperty = DependencyProperty.Register("HeaderWidth", typeof(double), typeof(PopupBehavior), new FrameworkPropertyMetadata(0.0, HeaderWidthChanged));
    public static readonly DependencyProperty PopupConnectionOffsetProperty = DependencyProperty.Register("PopupConnectionOffset", typeof(double), typeof(PopupBehavior), new FrameworkPropertyMetadata(0.0));

    #endregion Public fields

    #region Private fields

    private MethodInfo updateMethod;

    #endregion Private fields

    #region Public properties

    /// <summary>
    /// Gets or sets the Width of the control to subscribe for changes.
    /// </summary>
    public double HeaderWidth
    {
        get { return (double)GetValue(HeaderWidthProperty); }
        set { SetValue(HeaderWidthProperty, value); }
    }

    /// <summary>
    /// Gets or sets the offset of the connection visual of the popup.
    /// </summary>
    public double PopupConnectionOffset
    {
        get { return (double)GetValue(PopupConnectionOffsetProperty); }
        set { SetValue(PopupConnectionOffsetProperty, value); }
    }

    #endregion Public properties

    #region Public constructors

    /// <summary>
    /// Creates an instance of the <see cref="PopupBehavior" /> class.
    /// </summary>
    public PopupBehavior()
    {
        updateMethod = typeof(Popup).GetMethod("Reposition", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
    }

    #endregion Public constructors

    #region Protected methods

    /// <summary>
    /// Called after the behavior is attached to an AssociatedObject.
    /// </summary>
    protected override void OnAttached()
    {
        base.OnAttached();
        var pd = DependencyPropertyDescriptor.FromProperty(Popup.IsOpenProperty, typeof(Popup));
        pd.AddValueChanged(this.AssociatedObject, IsOpenChanged);
    }

    #endregion Protected methods

    #region Private methods

    /// <summary>
    /// The HeaderWidth property has changed.
    /// </summary>
    private static void HeaderWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var b = d as PopupBehavior;
        if (b != null)
            b.UpdateHorizontalOffset();
    }

    /// <summary>
    /// Gets the width of the associated popup.
    /// </summary>
    /// <returns>A double value; width of the popup.</returns>
    /// <remarks>
    /// This method gets the width of the popup's child, since the popup itself has a width of 0
    /// when collapsed.
    /// </remarks>
    /// <exception cref="InvalidOperationException">
    /// Occurs when the child of the popup is not derived from FrameworkElement.
    /// </exception>
    private double GetPopupWidth()
    {
        var child = this.AssociatedObject.Child as FrameworkElement;
        if (child != null)
            return child.ActualWidth;
        else
            throw new InvalidOperationException("Child of Popup is not derived from FrameworkElement");
    }

    /// <summary>
    /// The IsOpen property of the popup has changed.
    /// </summary>
    private void IsOpenChanged(object sender, EventArgs e)
    {
        if (this.AssociatedObject.IsOpen)
            UpdateHorizontalOffset();
    }

    /// <summary>
    /// Updates the HorizontalOffset of the popup.
    /// </summary>
    private void UpdateHorizontalOffset()
    {
        if (this.AssociatedObject.IsOpen)
        {
            var offset = (GetPopupWidth() - PopupConnectionOffset) * -1;
            if (this.AssociatedObject.HorizontalOffset == offset)
                updateMethod.Invoke(this.AssociatedObject, null);
            else
                this.AssociatedObject.HorizontalOffset = offset;
        }
    }

    #endregion Private methods
}

Make some changes in your XAML:

  1. Change the Placement property to "Bottom".
  2. Bind the PlacementTarget to your button (Edit) .
  3. Remove the HorizontalOffset and VerticalOffset properties from your Popup.
  4. Assign the Behavior.

Your code should look something like this:

...
xmlns:local="clr-namespace:MyProject"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
...

<Popup
    AllowsTransparency="True"
    IsOpen="{Binding IsChecked, ElementName=Edit}"
    Placement="Bottom"
    PlacementTarget="{Binding ElementName=Edit}"
    StaysOpen="False">
    <i:Interaction.Behaviors>
        <local:PopupBehavior
            HeaderWidth="{Binding ActualWidth, ElementName=tabTitle}"
            PopupConnectionOffset="36" />
    </i:Interaction.Behaviors>
    ...
</Popup>

Change the "local" clr-namespace to that of your project. If not already using, you might need to add a reference to the System.Windows.Interactivity assembly, I installed it using NuGet (Blend.Interactivity.Wpf).

The HeaderWidth purely subscribes to the changes of the header width of the TabItem and PopupConnectionOffset defines the offset from the right side of the Popup to the left side of the 'white stripe Border'.

Note that FrameworkElement should be assignable from the Popup's child value. Also, since the Popup is now targetted to the Edit button instead of the TabGrid, it aligns correctly when the TabItems exceed the parent container, preventing situations as these:

enter image description here

Community
  • 1
  • 1
Sjeijoet
  • 741
  • 4
  • 20
  • Changed the calling reflection method, as was suggested in [this comment](http://stackoverflow.com/questions/1600218/how-can-i-move-a-wpf-popup-when-its-anchor-element-moves?lq=1#comment33405237_4519727). – Sjeijoet Oct 08 '14 at 08:44
  • Can you upload the sample project to somewhere and paste the link in your answer? – Ron Oct 08 '14 at 13:26
  • Sure, I added another button though, to test whether it would still work: http://speedy.sh/BmBK5/WpfApplication.zip – Sjeijoet Oct 08 '14 at 13:37
  • The name "Interaction" does not exist in the namespace "clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" though I added it to the references. – Ron Oct 08 '14 at 14:05
  • XAML designer often shows weird errors like that in my projects, though they do build (and after a while they disappear). Can you run the application? If not, try this one: http://speedy.sh/nwnCy/WpfApplication.zip – Sjeijoet Oct 08 '14 at 14:13
  • I still see the same error message but it did build. The reason why it built "successful" is because you compiled it for net 4 or 4.5 and then for 3.5. Net 3.5 cannot use interactivity so it compiles your last working build which is the 4/4.5 one and that's why you think it works... Anyway, it still not as smooth as it should be – Ron Oct 08 '14 at 14:45
  • Ah thanks, I did not know that. I'm afraid there is no other solution to update the popup position other than using reflection or forcing the offset to update. Might be just my ignorance... – Sjeijoet Oct 08 '14 at 19:47