Подтвердить что ты не робот

Как я могу создать элемент управления тегами, похожий на evernote в wpf?

Мне нравится управление тегами в Evernote (версия Windows), и мне было интересно, есть ли что-то похожее там? Мне удалось найти элементы управления облаками тегов.

В частности, мне нравится набирать бесплатный формат, как в текстовом поле, которое просматривает и представляет стиль Intellisense, теги, которые соответствуют тому, что я набрал. Когда я выбираю тег, текст заменяется на кнопку, представляющую тег, текст текста которого является текстом тега.

Обновить: добавление скриншотов

Добавление нового тега

adding a tag

просмотр существующих тегов и щелчок 'x' для удаления тега

showing existing tags and delete by clicking 'x'

4b9b3361

Ответ 1

Это казалось действительно хорошим упражнением, поэтому я попытался создать этот элемент управления. Я не тестировал его полностью, дайте мне знать, хотите ли вы работать с ним и нуждаетесь в дальнейшей помощи.

enter image description here

enter image description here

enter image description here

enter image description here

Пример использования:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication1"
        xmlns:s="clr-namespace:System;assembly=mscorlib"
        Title="MainWindow" Height="350" Width="525">

    <Window.DataContext>
        <local:ViewModel />
    </Window.DataContext>

    <Grid>
        <!-- todo: implement ICommand properties on EvernoteTagControl to allow easy binding to the viewmodel. Alternatively, the user could use a behavior to handle TagClick, and if necessary TagAdded/TagRemoved -->
        <local:EvernoteTagControl ItemsSource="{Binding SelectedTags}" TagClick="TagControl_TagClick" >
            <local:EvernoteTagControl.AllTags>
                <s:String>read</s:String>
                <s:String>receipt</s:String>
                <s:String>recipe</s:String>
                <s:String>research</s:String>
                <s:String>restaurants</s:String>
            </local:EvernoteTagControl.AllTags>
        </local:EvernoteTagControl>
    </Grid>
</Window>

ViewModel:

using System.Collections.Generic;
using System.ComponentModel;

namespace WpfApplication1
{
    public class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private List<EvernoteTagItem> _selectedTags = new List<EvernoteTagItem>();
        public List<EvernoteTagItem> SelectedTags
        {
            get { return _selectedTags; }
            set
            {
                _selectedTags = value;
                if (_selectedTags != value)
                    OnPropertyChanged("SelectedTags");
            }
        }

        public ViewModel()
        {
            this.SelectedTags = new List<EvernoteTagItem>() { new EvernoteTagItem("news"), new EvernoteTagItem("priority") };
        }

        private void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

EvernoteTagControl:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;

namespace WpfApplication1
{
    [TemplatePart(Name = "PART_CreateTagButton", Type = typeof(Button))]
    public class EvernoteTagControl : ListBox
    {
        public event EventHandler<EvernoteTagEventArgs> TagClick;
        public event EventHandler<EvernoteTagEventArgs> TagAdded;
        public event EventHandler<EvernoteTagEventArgs> TagRemoved;

        static EvernoteTagControl()
        {
            // lookless control, get default style from generic.xaml
            DefaultStyleKeyProperty.OverrideMetadata(typeof(EvernoteTagControl), new FrameworkPropertyMetadata(typeof(EvernoteTagControl)));
        }

        public EvernoteTagControl()
        {
            //// some dummy data, this needs to be provided by user
            //this.ItemsSource = new List<EvernoteTagItem>() { new EvernoteTagItem("receipt"), new EvernoteTagItem("restaurant") };
            //this.AllTags = new List<string>() { "recipe", "red" };
        }

        // AllTags
        public List<string> AllTags { get { return (List<string>)GetValue(AllTagsProperty); } set { SetValue(AllTagsProperty, value); } }
        public static readonly DependencyProperty AllTagsProperty = DependencyProperty.Register("AllTags", typeof(List<string>), typeof(EvernoteTagControl), new PropertyMetadata(new List<string>()));


        // IsEditing, readonly
        public bool IsEditing { get { return (bool)GetValue(IsEditingProperty); } internal set { SetValue(IsEditingPropertyKey, value); } }
        private static readonly DependencyPropertyKey IsEditingPropertyKey = DependencyProperty.RegisterReadOnly("IsEditing", typeof(bool), typeof(EvernoteTagControl), new FrameworkPropertyMetadata(false));
        public static readonly DependencyProperty IsEditingProperty = IsEditingPropertyKey.DependencyProperty;

        public override void OnApplyTemplate()
        {
            Button createBtn = this.GetTemplateChild("PART_CreateTagButton") as Button;
            if (createBtn != null)
                createBtn.Click += createBtn_Click;

            base.OnApplyTemplate();
        }

        /// <summary>
        /// Executed when create new tag button is clicked.
        /// Adds an EvernoteTagItem to the collection and puts it in edit mode.
        /// </summary>
        void createBtn_Click(object sender, RoutedEventArgs e)
        {
            var newItem = new EvernoteTagItem() { IsEditing = true };
            AddTag(newItem);
            this.SelectedItem = newItem;
            this.IsEditing = true;

        }

        /// <summary>
        /// Adds a tag to the collection
        /// </summary>
        internal void AddTag(EvernoteTagItem tag)
        {
            if (this.ItemsSource == null)
                this.ItemsSource = new List<EvernoteTagItem>();

            ((IList)this.ItemsSource).Add(tag); // assume IList for convenience
            this.Items.Refresh();

            if (TagAdded != null)
                TagAdded(this, new EvernoteTagEventArgs(tag));
        }

        /// <summary>
        /// Removes a tag from the collection
        /// </summary>
        internal void RemoveTag(EvernoteTagItem tag, bool cancelEvent = false)
        {
            if (this.ItemsSource != null)
            {
                ((IList)this.ItemsSource).Remove(tag); // assume IList for convenience
                this.Items.Refresh();

                if (TagRemoved != null && !cancelEvent)
                    TagRemoved(this, new EvernoteTagEventArgs(tag));
            }
        }


        /// <summary>
        /// Raises the TagClick event
        /// </summary>
        internal void RaiseTagClick(EvernoteTagItem tag)
        {
            if (this.TagClick != null)
                TagClick(this, new EvernoteTagEventArgs(tag));
        }
    }

    public class EvernoteTagEventArgs : EventArgs
    {
        public EvernoteTagItem Item { get; set; }

        public EvernoteTagEventArgs(EvernoteTagItem item)
        {
            this.Item = item;
        }
    }
}

EvernoteTagItem:

using System.Collections;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace WpfApplication1
{
    [TemplatePart(Name = "PART_InputBox", Type = typeof(AutoCompleteBox))]
    [TemplatePart(Name = "PART_DeleteTagButton", Type = typeof(Button))]
    [TemplatePart(Name = "PART_TagButton", Type = typeof(Button))]
    public class EvernoteTagItem : Control
    {

        static EvernoteTagItem()
        {
            // lookless control, get default style from generic.xaml
            DefaultStyleKeyProperty.OverrideMetadata(typeof(EvernoteTagItem), new FrameworkPropertyMetadata(typeof(EvernoteTagItem)));
        }

        public EvernoteTagItem() { }
        public EvernoteTagItem(string text)
            : this()
        {
            this.Text = text;
        }

        // Text
        public string Text { get { return (string)GetValue(TextProperty); } set { SetValue(TextProperty, value); } }
        public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(EvernoteTagItem), new PropertyMetadata(null));

        // IsEditing, readonly
        public bool IsEditing { get { return (bool)GetValue(IsEditingProperty); } internal set { SetValue(IsEditingPropertyKey, value); } }
        private static readonly DependencyPropertyKey IsEditingPropertyKey = DependencyProperty.RegisterReadOnly("IsEditing", typeof(bool), typeof(EvernoteTagItem), new FrameworkPropertyMetadata(false));
        public static readonly DependencyProperty IsEditingProperty = IsEditingPropertyKey.DependencyProperty;

        /// <summary>
        /// Wires up delete button click and focus lost 
        /// </summary>
        public override void OnApplyTemplate()
        {
            AutoCompleteBox inputBox = this.GetTemplateChild("PART_InputBox") as AutoCompleteBox;
            if (inputBox != null)
            {
                inputBox.LostFocus += inputBox_LostFocus;
                inputBox.Loaded += inputBox_Loaded;
            }

            Button btn = this.GetTemplateChild("PART_TagButton") as Button;
            if (btn != null)
            {
                btn.Loaded += (s, e) =>
                {
                    Button b = s as Button;
                    var btnDelete = b.Template.FindName("PART_DeleteTagButton", b) as Button; // will only be found once button is loaded
                    if (btnDelete != null)
                    {
                        btnDelete.Click -= btnDelete_Click; // make sure the handler is applied just once
                        btnDelete.Click += btnDelete_Click;
                    }
                };

                btn.Click += (s, e) =>
                {
                    var parent = GetParent();
                    if (parent != null)
                        parent.RaiseTagClick(this); // raise the TagClick event of the EvernoteTagControl
                };
            }

            base.OnApplyTemplate();
        }

        /// <summary>
        /// Handles the click on the delete glyph of the tag button.
        /// Removes the tag from the collection.
        /// </summary>
        void btnDelete_Click(object sender, RoutedEventArgs e)
        {

            var item = FindUpVisualTree<EvernoteTagItem>(sender as FrameworkElement);
            var parent = GetParent();
            if (item != null && parent != null)
                parent.RemoveTag(item);

            e.Handled = true; // bubbling would raise the tag click event
        }

        /// <summary>
        /// When an AutoCompleteBox is created, set the focus to the textbox.
        /// Wire PreviewKeyDown event to handle Escape/Enter keys
        /// </summary>
        /// <remarks>AutoCompleteBox.Focus() is broken: http://stackoverflow.com/questions/3572299/autocompletebox-focus-in-wpf</remarks>
        void inputBox_Loaded(object sender, RoutedEventArgs e)
        {
            AutoCompleteBox acb = sender as AutoCompleteBox;
            if (acb != null)
            {
                var tb = acb.Template.FindName("Text", acb) as TextBox;
                if (tb != null)
                    tb.Focus();

                // PreviewKeyDown, because KeyDown does not bubble up for Enter
                acb.PreviewKeyDown += (s, e1) =>
                {
                    var parent = GetParent();
                    if (parent != null)
                    {
                        switch (e1.Key)
                        {
                            case (Key.Enter):  // accept tag
                                parent.Focus(); 
                                break;
                            case (Key.Escape): // reject tag
                                parent.Focus();
                                parent.RemoveTag(this, true); // do not raise RemoveTag event
                                break;
                        }
                    }
                };
            }
        }

        /// <summary>
        /// Set IsEditing to false when the AutoCompleteBox loses keyboard focus.
        /// This will change the template, displaying the tag as a button.
        /// </summary>
        void inputBox_LostFocus(object sender, RoutedEventArgs e)
        {
            this.IsEditing = false;
            var parent = GetParent();
            if (parent != null)
                parent.IsEditing = false;
        }

        private EvernoteTagControl GetParent()
        {
            return FindUpVisualTree<EvernoteTagControl>(this);
        }

        /// <summary>
        /// Walks up the visual tree to find object of type T, starting from initial object
        /// http://www.codeproject.com/Tips/75816/Walk-up-the-Visual-Tree
        /// </summary>
        private static T FindUpVisualTree<T>(DependencyObject initial) where T : DependencyObject
        {
            DependencyObject current = initial;
            while (current != null && current.GetType() != typeof(T))
            {
                current = VisualTreeHelper.GetParent(current);
            }
            return current as T;
        }
    }
}

Темы/Generic.xaml:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                            xmlns:local="clr-namespace:WpfApplication1"
                            xmlns:tkInput="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Input.Toolkit">

    <SolidColorBrush x:Key="HighlightBrush" Color="DodgerBlue" />

    <!-- EvernoteTagControl default style -->
    <Style TargetType="{x:Type local:EvernoteTagControl}">
        <Style.Resources>
            <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="White"/>
            <SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="White" />
            <LinearGradientBrush x:Key="IconBrush" EndPoint="0,1">
                <GradientStop Color="#5890f0" Offset="0" />
                <GradientStop Color="#0351d7" Offset="1" />
            </LinearGradientBrush>
        </Style.Resources>
        <Setter Property="FocusVisualStyle" Value="{x:Null}" />
        <Setter Property="VerticalAlignment" Value="Top" />
        <Setter Property="Margin" Value="5" />
        <Setter Property="MinHeight" Value="25" />
        <Setter Property="VerticalContentAlignment" Value="Center" />
        <Setter Property="SnapsToDevicePixels" Value="True" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:EvernoteTagControl}">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto" />
                            <ColumnDefinition Width="Auto" />
                            <ColumnDefinition Width="Auto" />
                        </Grid.ColumnDefinitions>
                        <Path Grid.Column="0" Margin="2" Fill="{StaticResource IconBrush}" Height="19" Stretch="Uniform" Data="M 50.535714,0.44196425 0.00446427,34.754464 l 0,106.906246 100.71874573,0 0,-107.124996 L 50.535714,0.44196425 z m 0.1875,21.21874975 c 6.311826,0 11.40625,5.094424 11.40625,11.40625 0,6.311826 -5.094424,11.4375 -11.40625,11.4375 -6.311826,0 -11.4375,-5.125674 -11.4375,-11.4375 0,-6.311826 5.125674,-11.40625 11.4375,-11.40625 z" />
                        <ItemsPresenter Grid.Column="1"  />
                        <Button Margin="5,0,0,0" Grid.Column="2" Content="Click to add tag..." x:Name="PART_CreateTagButton">
                            <Button.Template>
                                <ControlTemplate TargetType="Button">
                                    <ContentPresenter TextElement.Foreground="#FF555555" VerticalAlignment="Center" />
                                    <ControlTemplate.Triggers>
                                        <Trigger Property="IsMouseOver" Value="True">
                                            <Setter Property="Cursor" Value="Hand" />
                                        </Trigger>
                                    </ControlTemplate.Triggers>
                                </ControlTemplate>
                            </Button.Template>
                        </Button>
                    </Grid>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsEditing" Value="True">
                            <Setter TargetName="PART_CreateTagButton" Property="Visibility" Value="Collapsed" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="ItemContainerStyle">
            <Setter.Value>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="FocusVisualStyle" Value="{x:Null}" />
                </Style>
            </Setter.Value>
        </Setter>
        <Setter Property="ItemsPanel" >
            <Setter.Value>
                <ItemsPanelTemplate>
                    <StackPanel Orientation="Horizontal" />
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <!-- EvernoteTagItem default style -->
    <Style TargetType="{x:Type local:EvernoteTagItem}">
        <Setter Property="FocusVisualStyle" Value="{x:Null}" />
        <Setter Property="MinWidth" Value="50" />
        <Setter Property="Margin" Value="0,0,2,0" />
        <Setter Property="Padding" Value="5,2,0,2" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:EvernoteTagItem}">
                    <Button x:Name="PART_TagButton" Content="{TemplateBinding Text}" Margin="{TemplateBinding Margin}" Padding="{TemplateBinding Padding}">
                        <Button.Template>
                            <ControlTemplate TargetType="Button">
                                <Border Margin="{TemplateBinding Margin}" Padding="{TemplateBinding Padding}" BorderBrush="Gray" BorderThickness="1" CornerRadius="2" Background="#01FFFFFF">
                                    <Grid >
                                        <Grid.ColumnDefinitions>
                                            <ColumnDefinition Width="*" />
                                            <ColumnDefinition Width="Auto" />
                                        </Grid.ColumnDefinitions>
                                        <ContentPresenter VerticalAlignment="Center" HorizontalAlignment="Left" Margin="0,0,0,2" />
                                        <Button x:Name="PART_DeleteTagButton" Grid.Column="1"  Margin="3,0" VerticalAlignment="Center" HorizontalAlignment="Right" Visibility="Hidden"  >
                                            <Button.Template>
                                                <ControlTemplate>
                                                    <Grid Height="10" Width="10" Background="#01FFFFFF" >
                                                        <Path Stretch="Uniform" ClipToBounds="True" Stroke="{StaticResource HighlightBrush}" StrokeThickness="2" Data="M 85.364473,6.9977109 6.0640998,86.29808 6.5333398,85.76586 M 6.9926698,7.4977169 86.293043,86.79809 85.760823,86.32885"  />
                                                    </Grid>
                                                </ControlTemplate>
                                            </Button.Template>
                                        </Button>
                                    </Grid>
                                </Border>
                                <ControlTemplate.Triggers>
                                    <Trigger Property="IsMouseOver" Value="True">
                                        <Setter Property="Foreground" Value="{StaticResource HighlightBrush}" />
                                        <Setter TargetName="PART_DeleteTagButton" Property="Visibility" Value="Visible" />
                                    </Trigger>
                                </ControlTemplate.Triggers>
                            </ControlTemplate>
                        </Button.Template>
                    </Button>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <Trigger Property="IsEditing" Value="True">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type local:EvernoteTagItem}">
                            <tkInput:AutoCompleteBox x:Name="PART_InputBox"
                                                             Text="{Binding Text, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" 
                                                             ItemsSource="{Binding AllTags, RelativeSource={RelativeSource AncestorType={x:Type local:EvernoteTagControl}}}"
                                                             IsTextCompletionEnabled="True"
                                                             />
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Trigger>
        </Style.Triggers>
    </Style>

</ResourceDictionary>

Ответ 2

Вот как я собирался создать этот элемент управления.

Обзор высокого уровня:

Этот элемент управления будет содержать следующие основные компоненты: (1) AutoCompleteTextBox или AutoCompleteComboBox (2) элемент управления "button", который вы описываете (3). Коллекция пользовательского интерфейса для хранения применяемых тегов. Элементы AutoCompleteTextBox и коллекции для хранения применяемых тегов будут расположены раньше времени в контейнере макета по вашему выбору.

Во-первых, мы можем использовать AutoCompleteTextBox или AutoCompleteComboBox, чтобы предоставить нам параметры стиля Intellisense в качестве типов пользователей. Затем мы "слушаем" пользователя, который выберет представленный тег из раскрывающегося списка и динамически создаем новый элемент управления "button" (мы могли бы заранее создать UserControl/CustomControl для него, но нам нужно будет "новое", по крайней мере, один). "button" будет содержать в качестве текста текст AutoCompleteTextBox. Наконец, мы вставляем новый "button" в ListBox (или другой тип UU-коллекции), который содержит все применяемые в настоящее время теги.

Детали:

Есть несколько элементов управления AutoCompleteTextBox, но я опишу, как вы могли бы использовать этот CodeProject one. Этот примерный проект показывает, как вы можете использовать регулярные TextBox, AutoCompleteComboBox или AutoCompleteTextBox для достижения опций типа intellisense. Основными частями этого проекта являются AutoCompleteManager и DataProvider типа IAutoCompleteDataProvider, а также IAutoAppendDataProvider.

Прежде чем описывать дальнейшие подробности, вот несколько скриншотов этого элемента управления AutoCompleteTextBox в действии (заметьте, что я использую другой стиль для ListBoxItems, чем исходный автор). Свойство AutoAppend этого элемента управления является приятным прикосновением (оно включено для этого примера, поэтому после того, как я начну вводить текущее соответствие автоматически, "завершает" мое слово для меня). После ввода только "я":

After first typing a letter.

После наведения мыши над "Индианой":

After hovering mouse over "Indiana"

После нажатия на "Indiana":

After clicking on "Indiana"

Поскольку код из этого проекта обрабатывает раскрывающиеся варианты для нас, теперь нам нужно "слушать", когда пользователь выбирает элемент из раскрывающегося списка и соответственно создает новый элемент управления "button". Есть два основных случая, о которых я думаю, для этого нам нужно обращаться.

Первый случай - это когда пользователь выбирает элемент из списка с помощью мыши. Чтобы справиться с этим, мы могли бы вставить код для создания нового элемента управления "button" в обработчике MouseLeftButtonUp в AutoCompleteManager.cs, который находится вокруг строки 451:

    private void ListBox_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        ListBoxItem item = null;
        var d = e.OriginalSource as DependencyObject;
        while (d != null)
        {
            if (d is ListBoxItem)
            {
                item = d as ListBoxItem;
                break;
            }
            d = VisualTreeHelper.GetParent(d);
        }
        if (item != null)
        {
            _popup.IsOpen = false;
            UpdateText(item.Content as string, true);

            // User has selected an item with the mouse... 
            //  ** Add your new code HERE... something like:
            // 
            // TagButton tagButton = new TagButton(_textBox.Text); // _textBox is the TextBox to which the AutoCompleteManager has been applied
            // _autoCompleteTagControl.TagContainer.Add(tagButton); // _autoCompleteTagControl would be the control that we're making... it contains out other controls - I'm assuming we've passed it in or made it available.
        }
    }

Второй случай - это когда пользователь выбирает элемент из списка, нажав клавишу ввода. Чтобы справиться с этим, мы могли бы вставить аналогичный новый код в обработчик TextBox_PreviewKeyDown в AutoCompleteManager.cs, вокруг строки 291:

    private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        _supressAutoAppend = e.Key == Key.Delete || e.Key == Key.Back;
        if (!_popup.IsOpen)
        {
            return;
        }
        if (e.Key == Key.Enter)
        {
            _popup.IsOpen = false;
            _textBox.SelectAll();

            // User has selected an item by hitting the enter key...
            //  ** Add your new code HERE to create new TagButton, etc.
        }
        // ...
     }

Детали мелкой печати

Если вы решите использовать AutoCompleteTextBox из CodeProject, о котором я упоминал, вам может потребоваться применить несколько исправлений. Я столкнулся с двумя маленькими вещами после того, как я импортировал все в более крупный проект (они не произошли, когда просто запускал прилагаемый образец проекта). Первой была эта ошибка привязки:

System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.ItemsControl', AncestorLevel='1''. BindingExpression:Path=HorizontalContentAlignment; DataItem=null; target element is 'ListBoxItem' (Name=''); target property is 'HorizontalContentAlignment' (type 'HorizontalAlignment')

Другие столкнулись с этой проблемой с ListBoxes, описанной здесь. Как сказал один из ответов на этот пост, я смог включить явный стиль для HorizontalContentAligment и VerticalContentAlignment в моем стиле, чтобы решить эту проблему.

Вторая проблема возникла после того, как AutoCompleteTextBox был в приложении, которое включало вкладки. Когда у вас есть элементы управления в TabControl, вложенные элементы управления получат свое событие Loaded, которое возникает довольно часто - по крайней мере один раз для каждого нажатия на вкладку, содержащую элемент управления. Это вызвало неожиданные дополнительные вызовы методу AutoCompleteManager AttachTextBox(), в результате чего debug.Assert() завершился с ошибкой и произошли исключения (исходный автор предполагал, что событие Loaded будет только поднято один раз). Пока что единственное исправление, которое мне нужно было сделать для обработки, было в AutoCompeteTextBox.cs. Я добавил флаг _isInitialized, чтобы AttachTextBox вызывался только один раз:

    void AutoCompleteTextBox_Loaded(object sender, RoutedEventArgs e)
    {
        if (! _isInitialized)
        {
            _acm.AttachTextBox(this);
            _isInitialized = true;
        }
    }

Использование этого подхода должно позволить вам создать элемент управления, который ведет себя так, как вы описываете.