change set 48295

May 31, 2010 at 9:41 AM

Michael,

Is this somehow related to properties being updated with NULL values when DataTemplate is discarded?

If not, could you please give more info on when property changed events are fired incorrectly?

Thank you!

Coordinator
May 31, 2010 at 7:19 PM
Edited May 31, 2010 at 7:21 PM
The issue that I was seeing is that a TextBox was behaving improperly when the text changed, but data bound value was equal. This only happened when the text box binding's UpdateSourceTrigger is set to PropertyChanged.

To see this behavior, use a version of Update Controls prior to the fix, and create a TextBox bound to a decimal. Set the UpdateSourceTrigger to PropertyChanged. As you type, it will set the decimal property.

Now type "27." (decimal point at the end). As soon as you type the decimal point, the caret returns to the beginning of the line. The decimal point is not actually appended.

"27." is equal to "27" when converted to a decimal. The PropertyChanged event was fired when the decimal was "changed" from 27 to 27. This caused the TextBox to update the text to "27", erasing the decimal point. Because the text changed, the TextBox resets the caret. This change simply removes the PropertyChanged notification that would occur when the user types the decimal point.

This is a safe change, but it is not a perfect fix. Ideally, TextBox should ignore property changes while it has focus. When focused, the user is in control of the contents of a TextBox, not the application. This is how the Windows Forms version of Update Controls works. Unfortunately, I cannot alter the behavior of a WPF or Silverlight TextBox through data binding alone, so the XAML version remains broken.

May 31, 2010 at 11:02 PM

Oh yes, I have seen this and changed UpdateSourceTrigger to LostFocus. But I didn't give it much thought.

What troubles me though (and it is not Update Controls issue, but WPF binding in general) is I don't know how to keep DataTemplate alive while they are in use.

I have the following scenario:

TabControl with TabItems bound to a collection of ViewModels. Each tab item can be bound to a different type, but all inherit from some same base type.

When I switch tabs from one type to the other the previous tab fires property change (on ComboBox, DataGrid, ListView etc) with NULL values and ViewModel gets updated, UNLESS the two tabs are of the same type in this case DataTemplate is not discarded. I tried not to recycle templates but it doesn't help. 

So what happens is I have to check for NULLs in property setters. But if NULL is a valid selection then I have to write a lengthy workaround.

This behavior can be seen in famous Josh Smith's MVVM demo http://msdn.microsoft.com/en-us/magazine/dd419663.aspx, and I tried all sorts of things to avoid it, but it looks like the only thing for now is to check for NULLs.

Update Controls just pass through this behavior onto view model.

I was wondering if you had come across this in WPF and may be have some thoughts on how to best deal with it.

Thank you!

Dec 10, 2010 at 11:58 PM

I found solution to this problem if anyone is interested.

Let me reiterate the problem.

- a TabControl which tabs are bound to a dynamic collection.

- user adjusts or resizes UI controls on one tab, then switches to a tab of different type (this is important, since this is when WPF will recylce or dispose rendered content presenter since it will no longer match by type to the one shown on the screen => current tab)

- then user switches back to the first tab and all the UI controls are back to their original sizes, positions, sort settings on DataGrid, etc.

-----

Drop this class in your project, add a reference in your xaml and use it instead of regular tab control.

Basically the solution keeps instances of content presenters in a list while bound object is not removed from the collection. This way you don't have to move UI settings into your view model.

    [TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
    public class TabControlEx : TabControl
    {
        private Panel _itemsHolder = null;

        public TabControlEx()
            : base()
        {
            // this is necessary so that we get the initial databound selected item
            this.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
        }

        /// <summary>
        /// if containers are done, generate the selected item
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
        {
            if (this.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
            {
                this.ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;
                UpdateSelectedItem();
            }
        }

        /// <summary>
        /// get the ItemsHolder and generate any children
        /// </summary>
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            object temp = GetTemplateChild("PART_ItemsHolder");
            _itemsHolder = temp as Panel;
            UpdateSelectedItem();
        }

        /// <summary>
        /// when the items change we remove any generated panel children and add any new ones as necessary
        /// </summary>
        /// <param name="e"></param>
        protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
        {
            base.OnItemsChanged(e);

            if (_itemsHolder == null)
            {
                return;
            }

            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Reset:
                    _itemsHolder.Children.Clear();
                    break;

                case NotifyCollectionChangedAction.Add:
                case NotifyCollectionChangedAction.Remove:
                    if (e.OldItems != null)
                    {
                        foreach (var item in e.OldItems)
                        {
                            ContentPresenter cp = FindChildContentPresenter(item);
                            if (cp != null)
                            {
                                _itemsHolder.Children.Remove(cp);
                            }
                        }
                    }

                    // don't do anything with new items because we don't want to
                    // create visuals that aren't being shown

                    UpdateSelectedItem();
                    break;

                case NotifyCollectionChangedAction.Replace:
                    throw new NotImplementedException("Replace not implemented yet");
                    break;
            }
        }

        /// <summary>
        /// update the visible child in the ItemsHolder
        /// </summary>
        /// <param name="e"></param>
        protected override void OnSelectionChanged(SelectionChangedEventArgs e)
        {
            base.OnSelectionChanged(e);
            UpdateSelectedItem();
        }

        /// <summary>
        /// generate a ContentPresenter for the selected item
        /// </summary>
        void UpdateSelectedItem()
        {
            if (_itemsHolder == null)
            {
                return;
            }

            // generate a ContentPresenter if necessary
            TabItem item = GetSelectedTabItem();
            if (item != null)
            {
                CreateChildContentPresenter(item);
            }

            // show the right child
            foreach (ContentPresenter child in _itemsHolder.Children)
            {
                child.Visibility = ((child.Tag as TabItem).IsSelected) ? Visibility.Visible : Visibility.Collapsed;
            }
        }

        /// <summary>
        /// create the child ContentPresenter for the given item (could be data or a TabItem)
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        ContentPresenter CreateChildContentPresenter(object item)
        {
            if (item == null)
            {
                return null;
            }

            ContentPresenter cp = FindChildContentPresenter(item);

            if (cp != null)
            {
                return cp;
            }

            // the actual child to be added.  cp.Tag is a reference to the TabItem
            cp = new ContentPresenter();
            cp.Content = (item is TabItem) ? (item as TabItem).Content : item;
            cp.ContentTemplate = this.SelectedContentTemplate;
            cp.ContentTemplateSelector = this.SelectedContentTemplateSelector;
            cp.ContentStringFormat = this.SelectedContentStringFormat;
            cp.Visibility = Visibility.Collapsed;
            cp.Tag = (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
            _itemsHolder.Children.Add(cp);
            return cp;
        }

        /// <summary>
        /// Find the CP for the given object.  data could be a TabItem or a piece of data
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        ContentPresenter FindChildContentPresenter(object data)
        {
            if (data is TabItem)
            {
                data = (data as TabItem).Content;
            }

            if (data == null)
            {
                return null;
            }

            if (_itemsHolder == null)
            {
                return null;
            }

            foreach (ContentPresenter cp in _itemsHolder.Children)
            {
                if (cp.Content == data)
                {
                    return cp;
                }
            }

            return null;
        }

        /// <summary>
        /// copied from TabControl; wish it were protected in that class instead of private
        /// </summary>
        /// <returns></returns>
        protected TabItem GetSelectedTabItem()
        {
            object selectedItem = base.SelectedItem;
            if (selectedItem == null)
            {
                return null;
            }
            TabItem item = selectedItem as TabItem;
            if (item == null)
            {
                item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem;
            }
            return item;
        }
    }