Search code examples
c#wpfconstructoruser-controls

How To Prevent Constructor from being called on value change


I have a custom usercontrol that I'm using in a DataTemplate. In this usercontrol I have a text box. Previously when I was implementing this usercontrol I would have no problems but now that I've put it into a Datatemplate it seems that something has gone wrong.

Each time I type something into the text box the code runs normally but at the end of my triggers the usercontrols constructor gets called again and clears my textbox. (Hidden threads call it so I have no idea where to even start looking for where this unintuitive call originates from)

I'm trying to figure out what is causing this constructor to fire again. The other binds seem to work well and the correct information is being populated and displayed. It's just that constructor that is called again after everything resolves and clears the internal variables of the ui control.

Current Execution:

I type into the textbox. The triggers get my value of the text box filters the lists accordingly and than the constructor gets called and the textbox resets to default value of "".

Desired Execution:

I type into the textbox. The triggers get my value of the text box filters the lists accordingly.

<UserControl x:Class="Analytics_Module.Views.TenantProfileFilterFieldsView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:Analytics_Module.Views"
             xmlns:vm="clr-namespace:Analytics_Module.ViewModels"
             xmlns:uiComponents="clr-namespace:Analytics_Module.UI_Components"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <DockPanel>
        <DockPanel.DataContext>
            <vm:TenantProfileFilterFieldsViewModel  x:Name="test"/>
        </DockPanel.DataContext>

....

            <ScrollViewer HorizontalScrollBarVisibility="Auto">
                <ItemsControl ItemsSource="{Binding FiltersState.GroupedTenantNames, Mode=TwoWay}">
                    <ItemsControl.ItemsPanel>
                        <ItemsPanelTemplate>
                            <StackPanel Orientation="Horizontal" />
                        </ItemsPanelTemplate>
                    </ItemsControl.ItemsPanel>
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                                <uiComponents:neoCombobox 
                                LabelText="Tenant Names"
                                ListBoxItems="{Binding StaticLists.TenantNames, ElementName=test}"
                                DisplayListBoxItems ="{Binding}"
                                />
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>

CUSTOM USER CONTROL

<UserControl x:Class="Analytics_Module.UI_Components.neoCombobox"
             x:Name="parent"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:model="clr-namespace:Analytics_Module.Models"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <StackPanel DataContext="{Binding ElementName=parent}" Width="200">

        <Label Name="ComboboxLabel"
                Content="{Binding Path=LabelText, FallbackValue='Error'}" Margin="5"/>
        <TextBox Name="InputField"
                     Text="{Binding Path=TextBoxValue, Mode=TwoWay, FallbackValue='Error', UpdateSourceTrigger='PropertyChanged'}"/>

        <!--TODO rename -->
        <ListBox Name="Something"
                ItemsSource="{Binding Path=DisplayListBoxItems, FallbackValue={}, Mode=TwoWay}"  >
            <ListBox.ItemTemplate >
                <DataTemplate >
                    <StackPanel>
                        <CheckBox Margin="-1"
                                  Content="{Binding Name, FallbackValue='Error'}" 
                                  IsChecked="{Binding Check_Status, Mode=TwoWay, FallbackValue=true}">
                        </CheckBox>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </StackPanel>
</UserControl>

Back end of user contorl

using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Analytics_Module.Models;
using Analytics_Module.Utillity;
using System.Timers;
using System.Collections.Specialized;

namespace Analytics_Module.UI_Components
{
    /// <summary>
/// Interaction logic for neoCombobox.xaml
/// </summary>
    public partial class neoCombobox : UserControl
    {
        #region LabelText DP
        public String LabelText
        {
            get { return (String)GetValue(LabelTextProperty); }
            set { SetValue(LabelTextProperty, value); }
        }
        public static readonly DependencyProperty LabelTextProperty =
            DependencyProperty.Register("LabelText",
                typeof(string),
                typeof(neoCombobox), new PropertyMetadata("")
            );

        #endregion


        #region TextBoxValue DP

        /// <summary>
        /// Gets or sets the Value which is being displayed
        /// </summary>
        public String TextBoxValue
        {
            get { return (String)GetValue(TextBoxValueProperty); }
            set { SetValue(TextBoxValueProperty, value); }
        }
        /// <summary>
        /// Identified the TextBoxValue dependency property
        /// </summary>
        public static readonly DependencyProperty TextBoxValueProperty =
            DependencyProperty.Register("TextBoxValue",
                typeof(String),
                typeof(neoCombobox),
                new PropertyMetadata("")
            );

        #endregion


        #region ListBoxItems DP
        public ItemsChangeObservableCollection<MultiSelectDropDownListEntry> ListBoxItems
        {
            get { return (ItemsChangeObservableCollection<MultiSelectDropDownListEntry>)GetValue(ListBoxItemsProperty); }
            set { SetValue(ListBoxItemsProperty, value); }
        }
        public static readonly DependencyProperty ListBoxItemsProperty =
            DependencyProperty.Register("ListBoxItems",
                typeof(ItemsChangeObservableCollection<MultiSelectDropDownListEntry>),
                typeof(neoCombobox),
                new PropertyMetadata(new ItemsChangeObservableCollection<MultiSelectDropDownListEntry>())
            );

        #endregion

        #region DisplayListBoxItems DP
        public ItemsChangeObservableCollection<MultiSelectDropDownListEntry> DisplayListBoxItems
        {
            get {
                if (GetValue(DisplayListBoxItemsProperty) == null)
                {
                    SetValue(DisplayListBoxItemsProperty, new ItemsChangeObservableCollection<MultiSelectDropDownListEntry>());
                }
                return (ItemsChangeObservableCollection<MultiSelectDropDownListEntry>)GetValue(DisplayListBoxItemsProperty);
            }
            set { SetValue(DisplayListBoxItemsProperty, value); }
        }
        public static readonly DependencyProperty DisplayListBoxItemsProperty =
            DependencyProperty.Register("DisplayListBoxItems",
                typeof(ItemsChangeObservableCollection<MultiSelectDropDownListEntry>),
                typeof(neoCombobox),
                new PropertyMetadata(new ItemsChangeObservableCollection<MultiSelectDropDownListEntry>())
            );

        #endregion

        /// <summary>
        /// _timer is used to determine if a user has stopped typing. 
        /// The timer is started when a user starts typing again or 
        /// types for the first time. 
        /// </summary>
        private readonly Timer _timerKeyPress;

        /// <summary>
        /// _timer is used to determine if a user has left the  typing. 
        /// The timer is started when a user starts typing again or 
        /// types for the first time. 
        /// </summary>
        private readonly Timer _timerMouseLeave;

        public neoCombobox()
        {
            if (TextBoxValue != "") return;

            InitializeComponent();
            _timerKeyPress = new Timer();
            _timerKeyPress.Interval = 750;
            _timerKeyPress.Elapsed += new ElapsedEventHandler(UserPausedTyping);
            _timerKeyPress.AutoReset = false;


            _timerMouseLeave = new Timer();
            _timerMouseLeave.Interval = 550;
            //_timerMouseLeave.Elapsed += new ElapsedEventHandler(UserLeft);
            _timerMouseLeave.AutoReset = false;



        }

        //TODO Add property to determine if user preferes Mouse Leave of focus leave. 
        protected override void OnPreviewGotKeyboardFocus(KeyboardFocusChangedEventArgs e)
        {
            Console.WriteLine("@@@@@@@@@@@@@@@OnPreviewGotKeyboardFocus");
            _timerMouseLeave.Stop();
            base.OnPreviewGotKeyboardFocus(e);
        }

        protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
        {
            Console.WriteLine("------------OnPreviewLostKeyboardFocus");
            _timerMouseLeave.Stop();
            _timerMouseLeave.Start();
            base.OnPreviewLostKeyboardFocus(e);
        }

        protected override void OnMouseEnter(MouseEventArgs e)
        {
            _timerMouseLeave.Stop();
            base.OnMouseEnter(e);
        }

        protected override void OnMouseLeave(MouseEventArgs e)
        {
            _timerMouseLeave.Stop();
            _timerMouseLeave.Start();
            base.OnMouseLeave(e);

        }

        protected override void OnKeyUp(KeyEventArgs e)
        {
            _timerKeyPress.Stop();
            _timerKeyPress.Start();
        }

        private void UserPausedTyping(object source, ElapsedEventArgs e)
        {
            this.Dispatcher.Invoke(() =>
            {
                Console.WriteLine("@@@@@@@@@@@@@@@UserPausedTyping");
                this.RefreshDisplayList();
            });
        }

        private void UserLeft(object source, ElapsedEventArgs e)
        {
            this.Dispatcher.Invoke(() =>
            {
                Console.WriteLine("@@@@@@@@@@@@@@@User Left");
                this.TextBoxValue = "";
                this.RefreshDisplayList();
            });
        }

        protected void RefreshDisplayList()
        {
            int ItemsourceCount = 0;
            foreach (MultiSelectDropDownListEntry entry in this.DisplayListBoxItems.ToList())
            {
                if (!entry.Check_Status) this.DisplayListBoxItems.Remove(entry);
            }

            if (this.TextBoxValue == "") return;

            foreach (MultiSelectDropDownListEntry entry in this.ListBoxItems)
            {
                if (entry.Name.ToString().ToLower().Contains(this.TextBoxValue.ToLower()) && !this.DisplayListBoxItems.Contains(entry))
                {
                    this.DisplayListBoxItems.Add(entry);
                    if (ItemsourceCount++ > 15) break;
                }
            }
        }
    }
}

Solution

  • You cannot always avoid the recreation of containers by the ItemsControl, but you can make your text data persistent by binding the Text property of your TextBox to a property of the view model and not to a property of the custom control itself.

    Instead of writing:

    <TextBox Name="InputField" Text="{Binding Path=TextBoxValue, Mode=TwoWay, FallbackValue='Error', UpdateSourceTrigger='PropertyChanged'}"/>
    

    maybe write

    <TextBox Name="InputField" Text="{Binding MyTextProperty, Mode=TwoWay, FallbackValue='Error', UpdateSourceTrigger='PropertyChanged'}"/>
    

    and have the MyTextProperty defined in your view model like this:

    public class GroupedTenantNameViewModel {
        public string MyTextProperty { get; set; }
    }
    

    and make your FiltersState.GroupedTenantNames collection a collection of GroupedTenantNameViewModel items. This collection will be persistent even though the ItemsControl re-generates all items, and the binding will take care of putting data back in its place.

    If you're not using the MVVM pattern at all, then I suggest you research a bit into it as it's made to deal with bindings the right way!