如何在 WPF TabControl 中修复此行为?
How to fix this behavior in a WPF TabControl?
我有一个 WPF .NET Core 5.0 项目,其中有一个 TabControl
,我可以通过单击表示为 [+] 的 Button
添加一个新的 TabItem
。
MainWindow.cs:
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace WpfApp1
{
public partial class MainWindow : Window
{
int TabIndex = 1;
ObservableCollection<TabVM> Tabs =
new ObservableCollection<TabVM>();
public MainWindow()
{
InitializeComponent();
var tab1 = new TabVM()
{
Header = $"Tab {TabIndex}"
};
Tabs.Add(tab1);
AddNewPlusButton();
MyTabControl.ItemsSource = Tabs;
MyTabControl.SelectionChanged +=
MyTabControl_SelectionChanged;
}
private void MyTabControl_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
if (e.Source is TabControl)
{
var pos = MyTabControl.SelectedIndex;
if (pos != 0 && pos == Tabs.Count - 1) //last tab
{
var tab = Tabs.Last();
ConvertPlusToNewTab(tab);
AddNewPlusButton();
}
}
}
void ConvertPlusToNewTab(TabVM tab)
{
//Do things to make it a new tab.
TabIndex++;
tab.Header = $"Tab {TabIndex}";
tab.IsPlaceholder = false;
}
void AddNewPlusButton()
{
var plusTab = new TabVM()
{
Header = "+",
IsPlaceholder = true
};
Tabs.Add(plusTab);
}
class TabVM : INotifyPropertyChanged
{
string _Header;
public string Header
{
get => _Header;
set
{
_Header = value;
OnPropertyChanged();
}
}
bool _IsPlaceholder = false;
public bool IsPlaceholder
{
get => _IsPlaceholder;
set
{
_IsPlaceholder = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged([CallerMemberName] string property = "")
{
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(property));
}
}
private void OnTabCloseClick(object sender, RoutedEventArgs e)
{
var tab = (sender as Button).DataContext as TabVM;
if (Tabs.Count > 2)
{
var index = Tabs.IndexOf(tab);
if (index == Tabs.Count - 2)//last tab before [+]
{
MyTabControl.SelectedIndex--;
}
Tabs.RemoveAt(index);
}
}
}
}
XAML:
<TabControl x:Name="MyTabControl">
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Header, Mode=OneWay}"/>
<Button Click="OnTabCloseClick"
Width="20"
Padding="0"
Margin="8 0 0 0"
Content="X">
<Button.Style>
<Style TargetType="Button"
x:Key="CloseButtonStyle">
<Setter Property="Visibility"
Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsPlaceholder}"
Value="True">
<Setter Property="Visibility"
Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl>
<ContentControl.Resources>
<ContentControl x:Key="TabContentTemplate">
<Grid>
<Label Content="Enter your text here:"
HorizontalAlignment="Left"
Margin="30,101,0,0"
VerticalAlignment="Top"
Width="298"
FontSize="18"/>
<RichTextBox HorizontalAlignment="Left"
Height="191"
Margin="8,135,0,0"
VerticalAlignment="Top"
Width="330">
<FlowDocument/>
</RichTextBox>
</Grid>
</ContentControl>
</ContentControl.Resources>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding IsPlaceholder}"
Value="True">
<Setter Property="Content"
Value="{x:Null}"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsPlaceholder}"
Value="False">
<Setter Property="Content"
Value="{StaticResource TabContentTemplate}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
在XAML
中我在ContentControl
中设置了一个Grid
TabContentTemplate
,但是Grid
的任何控件都像那样RichTextBox
如果我更改其文本,使其反映到所有选项卡的 RichTextBox
。如何在其中添加 Grid
而不将其控制值反映到其他选项卡?
下面的 gif 显示了问题,无论我在 RichTextBox
中键入什么,都会反映在其他选项卡中。
一个简化的例子。
Collection 项目:
using Simplified;
namespace AddTabItem
{
public class TabVm : BaseInpc
{
string _header;
bool _isPlaceholder;
private string _text;
public string Header { get => _header; set => Set(ref _header, value); }
public bool IsPlaceholder { get => _isPlaceholder; set => Set(ref _isPlaceholder, value); }
public string Text { get => _text; set => Set(ref _text, value); }
}
}
视图模型:
using Simplified;
using System.Collections.ObjectModel;
namespace AddTabItem
{
public class TabsCollectionViewModel : BaseInpc
{
private TabVm _selectedTab;
private RelayCommand _addNewTabCommand;
private RelayCommand _removeTabCommand;
public ObservableCollection<TabVm> Tabs { get; } = new ObservableCollection<TabVm>();
public TabVm SelectedTab { get => _selectedTab; set => Set(ref _selectedTab, value); }
public RelayCommand AddNewTabCommand => _addNewTabCommand
?? (_addNewTabCommand = new RelayCommand(
() =>
{
TabVm tab = new TabVm() { Header = $"Tab{Tabs.Count}" };
Tabs.Add(tab);
SelectedTab = tab;
}));
public RelayCommand RemoveTabCommand => _removeTabCommand
?? (_removeTabCommand = new RelayCommand<TabVm>(
tab =>
{
int index = Tabs.IndexOf(tab);
if (index >= 0)
{
Tabs.RemoveAt(index);
if (index >= Tabs.Count)
index = Tabs.Count - 1;
if (index < 0)
SelectedTab = null;
else
SelectedTab = Tabs[index];
}
},
tab => Tabs.Contains(tab)));
}
}
Window XAML:
<Window x:Class="AddTabItem.AddTabExamleWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:AddTabItem"
mc:Ignorable="d"
Title="AddTabExamleWindow" Height="450" Width="800"
DataContext="{DynamicResource viewModel}">
<FrameworkElement.Resources>
<local:TabsCollectionViewModel x:Key="viewModel"/>
<local:TabVm x:Key="newTab"/>
<CollectionViewSource x:Key="tabsCollectionView"
Source="{Binding Tabs}"/>
<CompositeCollection x:Key="tabs">
<CollectionContainer Collection="{Binding Mode=OneWay, Source={StaticResource tabsCollectionView}}"/>
<StaticResource ResourceKey="newTab"/>
</CompositeCollection>
<DataTemplate x:Key="TabItem.HeaderTemplate"
DataType="{x:Type local:TabVm}">
<Grid>
<StackPanel Orientation="Horizontal">
<Panel.Style>
<Style TargetType="StackPanel">
<Style.Triggers>
<DataTrigger Binding="{Binding}" Value="{StaticResource newTab}">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Panel.Style>
<TextBlock Text="{Binding Header}"
Margin="2"/>
<Button Content="❌" FontWeight="Bold" Foreground="Red"
Command="{Binding RemoveTabCommand, Mode=OneWay, Source={StaticResource viewModel}}"
CommandParameter="{Binding Mode=OneWay}"/>
</StackPanel>
<Button Content="✚" FontWeight="Bold" Foreground="Green"
Command="{Binding AddNewTabCommand, Mode=OneWay, Source={StaticResource viewModel}}">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding}" Value="{StaticResource newTab}">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</Grid>
</DataTemplate>
<DataTemplate x:Key="TabItem.ContentTemplate"
DataType="{x:Type local:TabVm}">
<TextBox Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<TextBox.Style>
<Style TargetType="TextBox">
<Style.Triggers>
<DataTrigger Binding="{Binding}" Value="{StaticResource newTab}">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
</DataTemplate>
</FrameworkElement.Resources>
<Grid>
<TabControl ItemsSource="{DynamicResource tabs}"
ItemTemplate="{DynamicResource TabItem.HeaderTemplate}"
ContentTemplate="{DynamicResource TabItem.ContentTemplate}"
SelectedItem="{Binding SelectedTab, Mode=TwoWay}"/>
</Grid>
</Window>
为了消除歧义,我给出了示例中使用的类的代码:
BaseInpc:
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Simplified
{
/// <summary>Base class with implementation of the <see cref="INotifyPropertyChanged"/> interface.</summary>
public abstract class BaseInpc : INotifyPropertyChanged
{
/// <inheritdoc cref="INotifyPropertyChanged"/>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>The protected method for raising the event <see cref = "PropertyChanged"/>.</summary>
/// <param name="propertyName">The name of the changed property.
/// If the value is not specified, the name of the method in which the call was made is used.</param>
protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary> Protected method for assigning a value to a field and raising
/// an event <see cref = "PropertyChanged" />. </summary>
/// <typeparam name = "T"> The type of the field and assigned value. </typeparam>
/// <param name = "propertyFiled"> Field reference. </param>
/// <param name = "newValue"> The value to assign. </param>
/// <param name = "propertyName"> The name of the changed property.
/// If no value is specified, then the name of the method
/// in which the call was made is used. </param>
/// <remarks> The method is intended for use in the property setter. <br/>
/// To check for changes,
/// used the <see cref = "object.Equals (object, object)" /> method.
/// If the assigned value is not equivalent to the field value,
/// then it is assigned to the field. <br/>
/// After the assignment, an event is created <see cref = "PropertyChanged" />
/// by calling the method <see cref = "RaisePropertyChanged (string)" />
/// passing the parameter <paramref name = "propertyName" />. <br/>
/// After the event is created,
/// the <see cref = "OnPropertyChanged (string, object, object)" />
/// method is called. </remarks>
protected void Set<T>(ref T propertyFiled, T newValue, [CallerMemberName] string propertyName = null)
{
if (!object.Equals(propertyFiled, newValue))
{
T oldValue = propertyFiled;
propertyFiled = newValue;
RaisePropertyChanged(propertyName);
OnPropertyChanged(propertyName, oldValue, newValue);
}
}
/// <summary> The protected virtual method is called after the property has been assigned a value and after the event is raised <see cref = "PropertyChanged" />. </summary>
/// <param name = "propertyName"> The name of the changed property. </param>
/// <param name = "oldValue"> The old value of the property. </param>
/// <param name = "newValue"> The new value of the property. </param>
/// <remarks> Can be overridden in derived classes to respond to property value changes. <br/>
/// It is recommended to call the base method as the first operator in the overridden method. <br/>
/// If the overridden method does not call the base class, then an unwanted change in the base class logic is possible. </remarks>
protected virtual void OnPropertyChanged(string propertyName, object oldValue, object newValue) { }
}
}
中继命令:
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
namespace Simplified
{
#region Delegates for WPF Command Methods
public delegate void ExecuteHandler(object parameter);
public delegate bool CanExecuteHandler(object parameter);
#endregion
#region Класс команд - RelayCommand
/// <summary> A class that implements <see cref = "ICommand" />. <br/>
/// Implementation taken from <see href = "https://www.cyberforum.ru/wpf-silverlight/thread2390714-page4.html#post13535649" />
/// and added a constructor for methods without a parameter.</summary>
public class RelayCommand : ICommand
{
private readonly CanExecuteHandler canExecute;
private readonly ExecuteHandler execute;
private readonly EventHandler requerySuggested;
/// <inheritdoc cref="ICommand.CanExecuteChanged"/>
public event EventHandler CanExecuteChanged;
/// <summary> Command constructor. </summary>
/// <param name = "execute"> Command method to execute. </param>
/// <param name = "canExecute"> Method that returns the state of the command. </param>
public RelayCommand(ExecuteHandler execute, CanExecuteHandler canExecute = null)
: this()
{
this.execute = execute ?? throw new ArgumentNullException(nameof(execute));
this.canExecute = canExecute;
requerySuggested = (o, e) => Invalidate();
CommandManager.RequerySuggested += requerySuggested;
}
/// <inheritdoc cref="RelayCommand(ExecuteHandler, CanExecuteHandler)"/>
public RelayCommand(Action execute, Func<bool> canExecute = null)
: this
(
p => execute(),
p => canExecute?.Invoke() ?? true
)
{ }
private RelayCommand()
=> dispatcher = Application.Current.Dispatcher;
private readonly Dispatcher dispatcher;
/// <summary> The method that raises the event <see cref = "CanExecuteChanged" />. </summary>
public void RaiseCanExecuteChanged()
{
if (dispatcher.CheckAccess())
Invalidate();
else
dispatcher.BeginInvoke((Action)Invalidate);
}
private void Invalidate()
=> CanExecuteChanged?.Invoke(this, EventArgs.Empty);
/// <inheritdoc cref="ICommand.CanExecute(object)"/>
public bool CanExecute(object parameter) => canExecute?.Invoke(parameter) ?? true;
/// <inheritdoc cref="ICommand.Execute(object)"/>
public void Execute(object parameter) => execute?.Invoke(parameter);
}
#endregion
}
中继命令 :
using System;
using System.Windows.Input;
namespace Simplified
{
#region Delegates for WPF Command Methods
public delegate void ExecuteHandler<T>(T parameter);
public delegate bool CanExecuteHandler<T>(T parameter);
#endregion
/// <summary> RelayCommand implementation for generic parameter methods. </summary>
/// <typeparam name = "T"> Method parameter type. </typeparam>
public class RelayCommand<T> : RelayCommand
{
/// <inheritdoc cref="RelayCommand(ExecuteHandler, CanExecuteHandler)"/>
public RelayCommand(ExecuteHandler<T> execute, CanExecuteHandler<T> canExecute = null)
: base
(
p =>
{
if (p is T t)
execute(t);
},
p => (p is T t) && (canExecute?.Invoke(t) ?? true)
)
{ }
}
}
我有一个 WPF .NET Core 5.0 项目,其中有一个 TabControl
,我可以通过单击表示为 [+] 的 Button
添加一个新的 TabItem
。
MainWindow.cs:
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace WpfApp1
{
public partial class MainWindow : Window
{
int TabIndex = 1;
ObservableCollection<TabVM> Tabs =
new ObservableCollection<TabVM>();
public MainWindow()
{
InitializeComponent();
var tab1 = new TabVM()
{
Header = $"Tab {TabIndex}"
};
Tabs.Add(tab1);
AddNewPlusButton();
MyTabControl.ItemsSource = Tabs;
MyTabControl.SelectionChanged +=
MyTabControl_SelectionChanged;
}
private void MyTabControl_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
if (e.Source is TabControl)
{
var pos = MyTabControl.SelectedIndex;
if (pos != 0 && pos == Tabs.Count - 1) //last tab
{
var tab = Tabs.Last();
ConvertPlusToNewTab(tab);
AddNewPlusButton();
}
}
}
void ConvertPlusToNewTab(TabVM tab)
{
//Do things to make it a new tab.
TabIndex++;
tab.Header = $"Tab {TabIndex}";
tab.IsPlaceholder = false;
}
void AddNewPlusButton()
{
var plusTab = new TabVM()
{
Header = "+",
IsPlaceholder = true
};
Tabs.Add(plusTab);
}
class TabVM : INotifyPropertyChanged
{
string _Header;
public string Header
{
get => _Header;
set
{
_Header = value;
OnPropertyChanged();
}
}
bool _IsPlaceholder = false;
public bool IsPlaceholder
{
get => _IsPlaceholder;
set
{
_IsPlaceholder = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged([CallerMemberName] string property = "")
{
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(property));
}
}
private void OnTabCloseClick(object sender, RoutedEventArgs e)
{
var tab = (sender as Button).DataContext as TabVM;
if (Tabs.Count > 2)
{
var index = Tabs.IndexOf(tab);
if (index == Tabs.Count - 2)//last tab before [+]
{
MyTabControl.SelectedIndex--;
}
Tabs.RemoveAt(index);
}
}
}
}
XAML:
<TabControl x:Name="MyTabControl">
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Header, Mode=OneWay}"/>
<Button Click="OnTabCloseClick"
Width="20"
Padding="0"
Margin="8 0 0 0"
Content="X">
<Button.Style>
<Style TargetType="Button"
x:Key="CloseButtonStyle">
<Setter Property="Visibility"
Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsPlaceholder}"
Value="True">
<Setter Property="Visibility"
Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl>
<ContentControl.Resources>
<ContentControl x:Key="TabContentTemplate">
<Grid>
<Label Content="Enter your text here:"
HorizontalAlignment="Left"
Margin="30,101,0,0"
VerticalAlignment="Top"
Width="298"
FontSize="18"/>
<RichTextBox HorizontalAlignment="Left"
Height="191"
Margin="8,135,0,0"
VerticalAlignment="Top"
Width="330">
<FlowDocument/>
</RichTextBox>
</Grid>
</ContentControl>
</ContentControl.Resources>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding IsPlaceholder}"
Value="True">
<Setter Property="Content"
Value="{x:Null}"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsPlaceholder}"
Value="False">
<Setter Property="Content"
Value="{StaticResource TabContentTemplate}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
在XAML
中我在ContentControl
中设置了一个Grid
TabContentTemplate
,但是Grid
的任何控件都像那样RichTextBox
如果我更改其文本,使其反映到所有选项卡的 RichTextBox
。如何在其中添加 Grid
而不将其控制值反映到其他选项卡?
下面的 gif 显示了问题,无论我在 RichTextBox
中键入什么,都会反映在其他选项卡中。
一个简化的例子。
Collection 项目:
using Simplified;
namespace AddTabItem
{
public class TabVm : BaseInpc
{
string _header;
bool _isPlaceholder;
private string _text;
public string Header { get => _header; set => Set(ref _header, value); }
public bool IsPlaceholder { get => _isPlaceholder; set => Set(ref _isPlaceholder, value); }
public string Text { get => _text; set => Set(ref _text, value); }
}
}
视图模型:
using Simplified;
using System.Collections.ObjectModel;
namespace AddTabItem
{
public class TabsCollectionViewModel : BaseInpc
{
private TabVm _selectedTab;
private RelayCommand _addNewTabCommand;
private RelayCommand _removeTabCommand;
public ObservableCollection<TabVm> Tabs { get; } = new ObservableCollection<TabVm>();
public TabVm SelectedTab { get => _selectedTab; set => Set(ref _selectedTab, value); }
public RelayCommand AddNewTabCommand => _addNewTabCommand
?? (_addNewTabCommand = new RelayCommand(
() =>
{
TabVm tab = new TabVm() { Header = $"Tab{Tabs.Count}" };
Tabs.Add(tab);
SelectedTab = tab;
}));
public RelayCommand RemoveTabCommand => _removeTabCommand
?? (_removeTabCommand = new RelayCommand<TabVm>(
tab =>
{
int index = Tabs.IndexOf(tab);
if (index >= 0)
{
Tabs.RemoveAt(index);
if (index >= Tabs.Count)
index = Tabs.Count - 1;
if (index < 0)
SelectedTab = null;
else
SelectedTab = Tabs[index];
}
},
tab => Tabs.Contains(tab)));
}
}
Window XAML:
<Window x:Class="AddTabItem.AddTabExamleWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:AddTabItem"
mc:Ignorable="d"
Title="AddTabExamleWindow" Height="450" Width="800"
DataContext="{DynamicResource viewModel}">
<FrameworkElement.Resources>
<local:TabsCollectionViewModel x:Key="viewModel"/>
<local:TabVm x:Key="newTab"/>
<CollectionViewSource x:Key="tabsCollectionView"
Source="{Binding Tabs}"/>
<CompositeCollection x:Key="tabs">
<CollectionContainer Collection="{Binding Mode=OneWay, Source={StaticResource tabsCollectionView}}"/>
<StaticResource ResourceKey="newTab"/>
</CompositeCollection>
<DataTemplate x:Key="TabItem.HeaderTemplate"
DataType="{x:Type local:TabVm}">
<Grid>
<StackPanel Orientation="Horizontal">
<Panel.Style>
<Style TargetType="StackPanel">
<Style.Triggers>
<DataTrigger Binding="{Binding}" Value="{StaticResource newTab}">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Panel.Style>
<TextBlock Text="{Binding Header}"
Margin="2"/>
<Button Content="❌" FontWeight="Bold" Foreground="Red"
Command="{Binding RemoveTabCommand, Mode=OneWay, Source={StaticResource viewModel}}"
CommandParameter="{Binding Mode=OneWay}"/>
</StackPanel>
<Button Content="✚" FontWeight="Bold" Foreground="Green"
Command="{Binding AddNewTabCommand, Mode=OneWay, Source={StaticResource viewModel}}">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding}" Value="{StaticResource newTab}">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</Grid>
</DataTemplate>
<DataTemplate x:Key="TabItem.ContentTemplate"
DataType="{x:Type local:TabVm}">
<TextBox Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<TextBox.Style>
<Style TargetType="TextBox">
<Style.Triggers>
<DataTrigger Binding="{Binding}" Value="{StaticResource newTab}">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
</DataTemplate>
</FrameworkElement.Resources>
<Grid>
<TabControl ItemsSource="{DynamicResource tabs}"
ItemTemplate="{DynamicResource TabItem.HeaderTemplate}"
ContentTemplate="{DynamicResource TabItem.ContentTemplate}"
SelectedItem="{Binding SelectedTab, Mode=TwoWay}"/>
</Grid>
</Window>
为了消除歧义,我给出了示例中使用的类的代码: BaseInpc:
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Simplified
{
/// <summary>Base class with implementation of the <see cref="INotifyPropertyChanged"/> interface.</summary>
public abstract class BaseInpc : INotifyPropertyChanged
{
/// <inheritdoc cref="INotifyPropertyChanged"/>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>The protected method for raising the event <see cref = "PropertyChanged"/>.</summary>
/// <param name="propertyName">The name of the changed property.
/// If the value is not specified, the name of the method in which the call was made is used.</param>
protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary> Protected method for assigning a value to a field and raising
/// an event <see cref = "PropertyChanged" />. </summary>
/// <typeparam name = "T"> The type of the field and assigned value. </typeparam>
/// <param name = "propertyFiled"> Field reference. </param>
/// <param name = "newValue"> The value to assign. </param>
/// <param name = "propertyName"> The name of the changed property.
/// If no value is specified, then the name of the method
/// in which the call was made is used. </param>
/// <remarks> The method is intended for use in the property setter. <br/>
/// To check for changes,
/// used the <see cref = "object.Equals (object, object)" /> method.
/// If the assigned value is not equivalent to the field value,
/// then it is assigned to the field. <br/>
/// After the assignment, an event is created <see cref = "PropertyChanged" />
/// by calling the method <see cref = "RaisePropertyChanged (string)" />
/// passing the parameter <paramref name = "propertyName" />. <br/>
/// After the event is created,
/// the <see cref = "OnPropertyChanged (string, object, object)" />
/// method is called. </remarks>
protected void Set<T>(ref T propertyFiled, T newValue, [CallerMemberName] string propertyName = null)
{
if (!object.Equals(propertyFiled, newValue))
{
T oldValue = propertyFiled;
propertyFiled = newValue;
RaisePropertyChanged(propertyName);
OnPropertyChanged(propertyName, oldValue, newValue);
}
}
/// <summary> The protected virtual method is called after the property has been assigned a value and after the event is raised <see cref = "PropertyChanged" />. </summary>
/// <param name = "propertyName"> The name of the changed property. </param>
/// <param name = "oldValue"> The old value of the property. </param>
/// <param name = "newValue"> The new value of the property. </param>
/// <remarks> Can be overridden in derived classes to respond to property value changes. <br/>
/// It is recommended to call the base method as the first operator in the overridden method. <br/>
/// If the overridden method does not call the base class, then an unwanted change in the base class logic is possible. </remarks>
protected virtual void OnPropertyChanged(string propertyName, object oldValue, object newValue) { }
}
}
中继命令:
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
namespace Simplified
{
#region Delegates for WPF Command Methods
public delegate void ExecuteHandler(object parameter);
public delegate bool CanExecuteHandler(object parameter);
#endregion
#region Класс команд - RelayCommand
/// <summary> A class that implements <see cref = "ICommand" />. <br/>
/// Implementation taken from <see href = "https://www.cyberforum.ru/wpf-silverlight/thread2390714-page4.html#post13535649" />
/// and added a constructor for methods without a parameter.</summary>
public class RelayCommand : ICommand
{
private readonly CanExecuteHandler canExecute;
private readonly ExecuteHandler execute;
private readonly EventHandler requerySuggested;
/// <inheritdoc cref="ICommand.CanExecuteChanged"/>
public event EventHandler CanExecuteChanged;
/// <summary> Command constructor. </summary>
/// <param name = "execute"> Command method to execute. </param>
/// <param name = "canExecute"> Method that returns the state of the command. </param>
public RelayCommand(ExecuteHandler execute, CanExecuteHandler canExecute = null)
: this()
{
this.execute = execute ?? throw new ArgumentNullException(nameof(execute));
this.canExecute = canExecute;
requerySuggested = (o, e) => Invalidate();
CommandManager.RequerySuggested += requerySuggested;
}
/// <inheritdoc cref="RelayCommand(ExecuteHandler, CanExecuteHandler)"/>
public RelayCommand(Action execute, Func<bool> canExecute = null)
: this
(
p => execute(),
p => canExecute?.Invoke() ?? true
)
{ }
private RelayCommand()
=> dispatcher = Application.Current.Dispatcher;
private readonly Dispatcher dispatcher;
/// <summary> The method that raises the event <see cref = "CanExecuteChanged" />. </summary>
public void RaiseCanExecuteChanged()
{
if (dispatcher.CheckAccess())
Invalidate();
else
dispatcher.BeginInvoke((Action)Invalidate);
}
private void Invalidate()
=> CanExecuteChanged?.Invoke(this, EventArgs.Empty);
/// <inheritdoc cref="ICommand.CanExecute(object)"/>
public bool CanExecute(object parameter) => canExecute?.Invoke(parameter) ?? true;
/// <inheritdoc cref="ICommand.Execute(object)"/>
public void Execute(object parameter) => execute?.Invoke(parameter);
}
#endregion
}
中继命令
using System;
using System.Windows.Input;
namespace Simplified
{
#region Delegates for WPF Command Methods
public delegate void ExecuteHandler<T>(T parameter);
public delegate bool CanExecuteHandler<T>(T parameter);
#endregion
/// <summary> RelayCommand implementation for generic parameter methods. </summary>
/// <typeparam name = "T"> Method parameter type. </typeparam>
public class RelayCommand<T> : RelayCommand
{
/// <inheritdoc cref="RelayCommand(ExecuteHandler, CanExecuteHandler)"/>
public RelayCommand(ExecuteHandler<T> execute, CanExecuteHandler<T> canExecute = null)
: base
(
p =>
{
if (p is T t)
execute(t);
},
p => (p is T t) && (canExecute?.Invoke(t) ?? true)
)
{ }
}
}