开发者

IndexOutOfRangeException when changing selected TabItem twice

I have the following simple WPF-app:

<Window x:Class="TabControlOutOfRangeException.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
    <TabControl ItemsSource="{Binding ItemsSource}"
                SelectedIndex="{Binding SelectedIndex, IsAsync=True}" /开发者_如何学运维>
</Window>

with following simple code-behind:

using System.Collections.Generic;

namespace TabControlOutOfRangeException
{
    public partial class MainWindow
    {
        public List<string> ItemsSource { get; private set; }
        public int SelectedIndex { get; set; }

        public MainWindow()
        {
            InitializeComponent();

            ItemsSource = new List<string>{"Foo", "Bar", "FooBar"};

            DataContext = this;
        }
    }
}

When I click on the second tab ("Bar"), nothing is displayed. When I click again on any tab, I get an IndexOutOfRangeException. Setting IsAsync to False, the TabControl works.

Unfortunately, I have the requirement to query the user a "Save changes?" question when he leaves the current tab. So I wanted to set the SelectedIndex back to the old value within the set-property. Obviously this doesn't work. What am I doing wrong?

Update

I've subclassed the TabControl with the evil hack and it works for me. Here is the code of MainWindow.xaml:

<Window x:Class="TabControlOutOfRangeException.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:TabControlOutOfRangeException="clr-namespace:TabControlOutOfRangeException" Title="MainWindow" Height="350" Width="525">
    <Grid>
        <TabControlOutOfRangeException:PreventChangingTabsTabControl
            ItemsSource="{Binding ItemsSource}"
            SelectedIndex="{Binding SelectedIndex}"
            CanChangeTab="{Binding CanChangeTab}" Margin="0,0,0,51" />
        <CheckBox Content="CanChangeTab" IsChecked="{Binding CanChangeTab}" Margin="0,287,0,0" />
    </Grid>
</Window>

And here MainWindow.xaml.cs:

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

namespace TabControlOutOfRangeException
{
    public partial class MainWindow : INotifyPropertyChanged
    {
        public int SelectedIndex { get; set; }
        public List<string> ItemsSource { get; private set; }

        public MainWindow()
        {
            InitializeComponent();

            ItemsSource = new List<string> { "Foo", "Bar", "FooBar" };

            DataContext = this;
        }

        private bool _canChangeTab;
        public bool CanChangeTab
        {
            get { return _canChangeTab; }
            set
            {
                _canChangeTab = value;
                OnPropertyChanged("CanChangeTab");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged(string property)
        {
            var handler = PropertyChanged;

            if (handler != null)
                handler(this, new PropertyChangedEventArgs(property));
        }
    }
}

And finally the subclassed TabControl:

using System;
using System.Windows;
using System.Windows.Controls;

namespace TabControlOutOfRangeException
{
    public class PreventChangingTabsTabControl : TabControl
    {
        private int _previousTab;

        public PreventChangingTabsTabControl()
        {
            SelectionChanged += (s, e) =>
            {
                if (!CanChangeTab)
                {
                    e.Handled = true;
                    SelectedIndex = _previousTab;
                }
                else
                    _previousTab = SelectedIndex;
            };
        }

        public static readonly DependencyProperty CanChangeTabProperty = DependencyProperty.Register(
            "CanChangeTab",
            typeof(Boolean),
            typeof(PreventChangingTabsTabControl)
        );

        public bool CanChangeTab
        {
            get { return (bool)GetValue(CanChangeTabProperty); }
            set { SetValue(CanChangeTabProperty, value); }
        }
    }
}


I'd consider a redesign of that window instead of introducing a heap of new problems by just trial-and-erroring on the "IsAsync" property of the binding.

I am not sure if a tab control will allow this level of control you seek. You could try to catch the event when someone tries to change the selected item, but you would not be able to cancel it out. There is a way however, see Option 4 if you dont want to read the other suggestions.

Option 1: The custom control

I would consider writing a bit of custom code that mimics the functionality of an item container. Its easy to achieve your desired behaviour this way. Just bind a command to the buttons (or whatever control you wish the user to click on), and return CanExecute with false if there are still changes to be submitted - or ask your user whatever you want when it gets executed, and only change the content displayed (ie your custom "TabItem") if desired.

Option 2: Preventing the user by disabling the tabs

Another way would be to bind the "IsEnabled" property of each of the tabitems to a dependency property on your viewmodel, that controls which of them is available to the user. Like, you know that the first page still needs work, just disable all the other ones meanwhile. But be aware that right now you are not creating any TabItems - your content are just plain strings.

public List<TabItem> ItemsSource { get; private set; }

....

ItemsSource = new List<TabItem> { new TabItem() { Header = "Foo", Content = "Foo" }, new TabItem() { Header = "Bar", Content = "Bar" }, new TabItem() { Header = "FooBar", Content = "FooBar" } };

Since you don't want to prevent the user doing something but rather would like to ask to save the changes, i'd go for the custom control route. Still there is option 3.

Option 3: Popup window

Use a popup window and ask to save changes if the user is finished with changing whatever is on that page and clicks on the "Close" button (rather than the "Save" button that should also reside on the same page ;) )

Option 4: Check on StackOverflow

Actually i did that for you, and here is a solution another user has found for the exact same problem: WPF Tab Control Prevent Tab Change The reason why i didnt post that up-front was that i personally wouldnt do it that way because, man do i HATE applications that do this.

Here you go.


Try actually implementing the SelectedIndex

    namespace TabControlOutOfRangeException
    {
        public partial class MainWindow  
        {
            public List<string> ItemsSource { get; private set; }
            private int selectedIndex

            public int SelectedIndex { 
                get { return selectedIndex; } 
                set { selecectedIndex = value; } }

            public MainWindow()
            {
                InitializeComponent();

                ItemsSource = new List<string>{"Foo", "Bar", "FooBar"};

                DataContext = this;
            }
        }
    }


If you want to be able to affect the TabControl the binding needs to be two-way, i.e. your code-behind needs to be able to notify the view that the property changed, for that you should implement INotifyPropertyChanged in your window, e.g.

public partial class MainWindow : INotifyPropertyChanged
private int _selectedIndex;
public int SelectedIndex
{
    get { return _selectedIndex; }
    set
    {
        if (_selectedIndex != value)
        {
            _selectedIndex = value;
            OnPropertyChanged("SelectedIndex");
        }
    }
}

public event PropertyChangedEventHandler PropertyChanged;

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

Async bindings are usually for properties which have a long-running getter, with e.g. a database query, you should not need this here.


In case you want to to change the selectedIndex in the setter itself, then to get it updated on UI, you have to raise the property changed in an async manner like this -

public partial class MainWindow : INotifyPropertyChanged

private int _selectedIndex;
public int SelectedIndex
{
    get { return _selectedIndex; }
    set
    {
        if (_selectedIndex != value)
        {
            _selectedIndex = value;
            OnPropertyChangedAsAsync("SelectedIndex");
        }
    }
}

public event PropertyChangedEventHandler PropertyChanged;

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

protected virtual void OnPropertyChangedAsAsync(string propertyName)
{
    Dispatcher.CurrentDispatcher.BeginInvoke((Action)delegate {  OnPropertyChanged(propertyName); }, DispatcherPriority.Render, null);
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜