xceedsoftware / wpftoolkit

All the controls missing in WPF. Over 1 million downloads.
Other
3.9k stars 878 forks source link

DockingManager : LostFocus event on textbox not firing (or too late) when changing tab #1641

Open houihoui opened 3 years ago

houihoui commented 3 years ago

Hi, I am using DockingManager following a mvvm pattern, like this:

<xcad:DockingManager DocumentsSource="{Binding Documents}">

Documents property is in my main vm :

    public class MainViewModel : ViewModelBase
    {
        public MainViewModel()
        {
            _documents.Add(new DocumentViewModel() { DocTitle = "Doc 1", Name = "Test 1", Length=3026556 });
            _documents.Add(new DocumentViewModel() { DocTitle = "Doc 2", Name = "Test 2", Length = 8488 });
        }

        #region Documents
        readonly ObservableCollection<object> _documents = new ObservableCollection<object>();

        ReadOnlyObservableCollection<object> _readonlyDocuments;

        public ReadOnlyObservableCollection<object> Documents
        {
            get
            {
                if (_readonlyDocuments == null)
                    _readonlyDocuments = new ReadOnlyObservableCollection<object>(_documents);

                return _readonlyDocuments;
            }
        }
        #endregion
    }

i have a userctrl as datatemplate for each DocumentViewModel binded to properties in a LostFocus mode:

<UserControl>
<Grid>
<TextBlock VerticalAlignment="Center">Name</TextBlock>
<TextBox Text="{Binding Name, UpdateSourceTrigger=LostFocus}" Grid.Column="1" MinWidth="100" Margin="5" LostFocus="TextBox_LostFocus"></TextBox>
<TextBlock Grid.Row="1" VerticalAlignment="Center">Length</TextBlock>
<TextBox Grid.Row="1" Text="{Binding Length, UpdateSourceTrigger=LostFocus}" Grid.Column="1" MinWidth="100" Margin="5" LostFocus="TextBox_LostFocus"></TextBox>

giving something like: image i change the first textbox content, adding any char: image then i click on the other tab: image if i want to comme back to the first tab, my changes have disapeared! image

however if i change the text in the first textbox then i click on the other textbox just below, firing the LostFocus event and updating the source, the changes remain as expected, so i think there's a bug with the LostFocus event when clicking directly on other tab.

XceedBoucherS commented 3 years ago

Hi,

I've build a sample based on your description. When I modify the "Name" TextBox and click the next Tab, I can see a LostFocus output. Then clicking back on first Tab shows the modified text in the "Name" TextBox. Here's my sample. Can you try it out and confirm ? I used the latest v4.0.2 of Toolkit from NuGet.

MainWindow.xaml: `

` MainWindow.xaml.cs: ` public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.DataContext = new MainViewModel(); } } public class MainViewModel// : ViewModelBase { public MainViewModel() { _documents.Add( new DocumentViewModel() { DocTitle = "Doc 1", Name = "Test 1", Length = 3026556 } ); _documents.Add( new DocumentViewModel() { DocTitle = "Doc 2", Name = "Test 2", Length = 8488 } ); } #region Documents readonly ObservableCollection _documents = new ObservableCollection(); ReadOnlyObservableCollection _readonlyDocuments; public ReadOnlyObservableCollection Documents { get { if( _readonlyDocuments == null ) _readonlyDocuments = new ReadOnlyObservableCollection( _documents ); return _readonlyDocuments; } } #endregion }` DocumentViewModel.xaml: ` Name Length ` DocumentViewModel.xaml.cs: `public partial class DocumentViewModel : UserControl { public DocumentViewModel() { InitializeComponent(); this.DataContext = this; } public string DocTitle { get; set; } public string Name { get; set; } public int Length { get; set; } private void TextBox_LostFocus( object sender, RoutedEventArgs e ) { System.Diagnostics.Debug.WriteLine("Lost Focus"); } }`
houihoui commented 3 years ago

Hi, thanks for the answer.

I confirm that your sample is indeed working (the LostFocus event fires when you change tabs), but the viewmodel you are using is a usercontrol.

For obvious separation reasons, my vm is a simple object which implements INotifyPropertyChanged like this:

    public class DocumentViewModel : INotifyPropertyChanged
    {
        public DocumentViewModel()
        {
        }

        string docNameValue;

        public string DocName
        {
            get
            {
                return this.docNameValue;
            }

            set
            {
                if (value != this.docNameValue)
                {
                    this.docNameValue = value;
                    NotifyPropertyChanged();
                }
            }
        }

        string docTitleValue;

        public string DocTitle
        {
            get
            {
                return this.docTitleValue;
            }

            set
            {
                if (value != this.docTitleValue)
                {
                    this.docTitleValue = value;
                    NotifyPropertyChanged();
                }
            }
        }

        int lengthValue;

        public int Length
        {
            get
            {
                return this.lengthValue;
            }

            set
            {
                if (value != this.lengthValue)
                {
                    this.lengthValue = value;
                    NotifyPropertyChanged();
                }
            }
        }

        bool isPdf;

        public bool IsPdf
        {
            get
            {
                return this.isPdf;
            }

            set
            {
                if (value != this.isPdf)
                {
                    this.isPdf = value;
                    NotifyPropertyChanged();
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

And when you bind it via the Documents property of the main vm, i have a LayoutItemTemplateSelector that returns the UserControl (here it's DocCtrl)

<ResourceDictionary>
    <DataTemplate x:Key="DocDataTemplate">
        <local:DocCtrl/>
    </DataTemplate>
    <local:TemplateSelector x:Key="DockingManagerTemplateSelector"
                            DocumentTemplate="{StaticResource DocDataTemplate}"/>
</ResourceDictionary>
<xcad:DockingManager DocumentsSource="{Binding Documents}"
                             LayoutItemTemplateSelector="{StaticResource DockingManagerTemplateSelector}">
            <xcad:DockingManager.DocumentHeaderTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Content.DocTitle}" />
                    </StackPanel>
                </DataTemplate>
            </xcad:DockingManager.DocumentHeaderTemplate>
    public class TemplateSelector : DataTemplateSelector
    {
        public TemplateSelector() { }

        public DataTemplate DocumentTemplate { get; set; }

        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            //check if the item is an instance of DocumentViewModel
            if (item is DocumentViewModel)
                return DocumentTemplate;

            //delegate the call to base class
            return base.SelectTemplate(item, container);
        }
    }

It's not very usefull in that case since there is just one type of vm, but in my real app, it is. In that case the LostFocus event is fired too late when you change tabs (when it is fired, there isn't any datacontext attached to it). The full project is in attachment.

LostFocusPbWpfApp.zip

XceedBoucherS commented 3 years ago

Hi, Thank you for the clarification. We'll need to investigate on this.

XceedBoucherS commented 3 years ago

I invite you to test your sample with a standard TabControl. The same thing happens. This could be a TabControl problem....or in the way your sample is built: <TabControl ItemsSource="{Binding Documents}" ContentTemplateSelector="{StaticResource DockingManagerTemplateSelector}" SelectedIndex="0"/>

houihoui commented 3 years ago

You're right, the same thing happens in a standard tabcontrol! I can't imagine what could be causing this in my sample since it is really really simple, on the other hand it would mean that this bug is in wpf framework since 2006?

houihoui commented 3 years ago

Hi, did a little digging for this problem, if the subject is interesting for anyone. It turns out that "The problem is not that it is not changing focus on the form, the problem is that it is setting the DataContext on the view to null before it changes focus, so the binding can no longer write the current value to the view model." as it is said in that link: https://groups.google.com/g/wpf-disciples/c/HKUU61A5l74?pli=1 this problem must be solved by implementing your own derived tabcontrol since it hasn't been adressed by MS