Newbie to WPF. I am attempting to make the tab flash of an AvalonDock DocumentHeader when a dependency property value is updated 'HasProblem' is set to true. Below is a code sample I understand that I need to create a DocumentHeaderTemplate and use DataTemplate.Triggers, perhaps a DataTrigger bound to HasProblem but am really unsure on implementation steps. Also if anyone could recommend a comprehensive video tutorial on WPF that perhaps covers some of the techniques that would be employed to achieve the flashing tab I would be very grateful.
<Window x:Class="AirportTab.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ad="http://schemas.xceed.com/wpf/xaml/avalondock"
xmlns:local="clr-namespace:AirportTab"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d:DataContext="{d:DesignInstance Type=local:AirportCodeDataSource, IsDesignTimeCreatable=True}"
Title="MainWindow" Height="300" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button Click="OnToggleErrorClick">
<StackPanel Orientation="Horizontal">
<Label Content="{Binding HasProblem}" ContentStringFormat="HasProblem={0}. Click to change" />
</StackPanel>
</Button>
<ad:DockingManager Grid.Row="1">
<ad:LayoutRoot>
<ad:LayoutPanel Orientation="Horizontal">
<ad:LayoutDocumentPaneGroup>
<ad:LayoutDocumentPane>
<ad:LayoutDocument Title="Airport Code">
<StackPanel>
<DataGrid ItemsSource="{Binding View}" />
</StackPanel>
</ad:LayoutDocument>
</ad:LayoutDocumentPane>
</ad:LayoutDocumentPaneGroup>
</ad:LayoutPanel>
</ad:LayoutRoot>
</ad:DockingManager>
</Grid>
</Window>
public class AirportCode
{
public string Name { get; set; }
public bool Operational { get; set; }
}
public class AirportCodeDataSource : DependencyObject
{
public AirportCodeDataSource()
{
this.View = new ObservableCollection<AirportCode>
{
new AirportCode {Name = "JFK", Operational = true},
new AirportCode {Name = "LHR", Operational = true},
new AirportCode {Name = "LGW", Operational = true}
};
}
public static readonly DependencyProperty HasProblemProperty =
DependencyProperty.Register(
"HasProblem",
typeof(bool),
typeof(AirportCodeDataSource));
public bool HasProblem
{
get { return (bool)GetValue(HasProblemProperty); }
set { SetValue(HasProblemProperty, value); }
}
public ObservableCollection<AirportCode> View { get; set; }
}
public partial class MainWindow : Window
{
public MainWindow()
{
this.DataContext = new AirportCodeDataSource();
InitializeComponent();
}
private void OnToggleErrorClick(object sender, RoutedEventArgs e)
{
this.AirportCodeDataSource.HasProblem = !this.AirportCodeDataSource.HasProblem;
}
public AirportCodeDataSource AirportCodeDataSource { get { return this.DataContext as AirportCodeDataSource; }}
}
You can create a DataTemplate for the DocumentHeaderTemplate with little animation. refer the below code.
<ad:DockingManager Grid.Row="1" >
<ad:DockingManager.DocumentHeaderTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Title}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="Transparent"/>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.HasProblem}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard Name="StartBlinking">
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Background).(SolidColorBrush.Color)" To="Orange" Duration="00:00:00.4" RepeatBehavior="Forever" AutoReverse="True"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.HasProblem}" Value="False">
<DataTrigger.EnterActions>
<RemoveStoryboard BeginStoryboardName="StartBlinking"/>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</DataTemplate>
</ad:DockingManager.DocumentHeaderTemplate>
<ad:LayoutRoot>
<ad:LayoutPanel Orientation="Horizontal">
<ad:LayoutDocumentPaneGroup>
<ad:LayoutDocumentPane>
<ad:LayoutDocument Title="Airport Code">
<StackPanel>
<DataGrid ItemsSource="{Binding View}" />
</StackPanel>
</ad:LayoutDocument>
</ad:LayoutDocumentPane>
</ad:LayoutDocumentPaneGroup>
</ad:LayoutPanel>
</ad:LayoutRoot>
</ad:DockingManager>
public partial class Window5 : Window
{
public AirportCodeDataSource AirportCodeDataSource { get { return this.DataContext as AirportCodeDataSource; } }
public Window5()
{
InitializeComponent();
this.DataContext = new AirportCodeDataSource();
this.AirportCodeDataSource.HasProblem = true;
}
}
public class AirportCode
{
public string Name { get; set; }
public bool Operational { get; set; }
}
public class AirportCodeDataSource : DependencyObject
{
public AirportCodeDataSource()
{
this.View = new ObservableCollection<AirportCode>
{
new AirportCode {Name = "JFK", Operational = true},
new AirportCode {Name = "LHR", Operational = true},
new AirportCode {Name = "LGW", Operational = true}
};
}
public static readonly DependencyProperty HasProblemProperty =
DependencyProperty.Register(
"HasProblem",
typeof(bool),
typeof(AirportCodeDataSource));
public bool HasProblem
{
get { return (bool)GetValue(HasProblemProperty); }
set { SetValue(HasProblemProperty, value); }
}
public ObservableCollection<AirportCode> View { get; set; }
}
Related
I have made a reusable UserControl in my WPF application that has a Textbox (and in the real project other components) inside of it. I'm using Fluent Validation with INotifyDataErrorInfo to validate user input in TextBoxes. My problem is that when the model whose property is bound to the UserControl's TextBox has errors, the TextBox's style doesn't trigger according to the style that is set. It seems that my style's trigger for the UserControl's Textbox can't read the Validation.HasError value from the model correctly. So is there a way to get the style to trigger and get the error tooltip for the UserControl's Textbox?
This question has been asked by several other people over the years, and I have looked at every single one of them, but none of them really work for me. One thing I tried that does work is a general ValidationRule in the UserControl.xaml for the textbox binding, but that doesn't allow for model specific rules. I'm hoping that some WPF guru will finally take the challenge and solve this problem! :)
If you make a sample project from the code I provided, and set the Height property to less than 10, you see that the normal TextBox gets the triggered errorstyle with the tooltip, but the UserControl's TextBox gets the basic red border:
Sample app with the cursor over the first textbox.
Here is my simplified code:
The UserControl:
<UserControl x:Class="UserControlValidationTest.DataInputUserControl"
x:Name="parentControl"
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"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<Style TargetType="TextBox" x:Key="TextBoxStyle">
<Style.Triggers>
<Trigger Property= "Validation.HasError" Value="true">
<Setter Property="Background" Value="Pink"/>
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<StackPanel Orientation="Horizontal" DataContext="{Binding ElementName=parentControl}">
<TextBox Name="ValueBox" Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" Width="60" Style="{StaticResource TextBoxStyle}"/>
</StackPanel>
public partial class DataInputUserControl : UserControl
{
public DataInputUserControl()
{
InitializeComponent();
Validation.SetValidationAdornerSite(this, ValueBox);
}
public double Value
{
get => (double)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(double), typeof(DataInputUserControl), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
}
MainWindow.xaml:
<Window x:Class="UserControlValidationTest.MainWindow"
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:UserControlValidationTest"
mc:Ignorable="d"
Title="MainWindow" Height="100" Width="200">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property= "Validation.HasError" Value="true">
<Setter Property="Background" Value="Pink"/>
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<StackPanel>
<TextBox Text="{Binding User.Height, UpdateSourceTrigger=PropertyChanged}" Width="60" Margin="10"/>
<local:DataInputUserControl Value="{Binding User.Height}" HorizontalAlignment="Center"/>
</StackPanel>
View model:
public class MainWindowViewModel
{
public UserModel User { get; set; }
public MainWindowViewModel()
{
User = new UserModel();
}
}
User Model:
public class UserModel : ValidatableModel
{
public double Height { get => GetPropertyValue<double>(); set => SetPropertyValue(value); }
public UserModel()
{
ModelValidator = new UserValidator();
Height = 180;
}
}
User Validator:
public class UserValidator : AbstractValidator<UserModel>
{
public UserValidator()
{
RuleFor(x => x.Height)
.GreaterThan(10);
}
}
Validatable Model:
using FluentValidation;
using FluentValidation.Results;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
public abstract class ValidatableModel : INotifyDataErrorInfo, INotifyPropertyChanged
{
private readonly Dictionary<string, object> propertyBackingDictionary = new Dictionary<string, object>();
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string parameter = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(parameter));
}
protected T GetPropertyValue<T>([CallerMemberName] string propertyName = null)
{
if (propertyBackingDictionary.TryGetValue(propertyName, out object value))
{
return (T)value;
}
return default(T);
}
protected bool SetPropertyValue<T>(T newValue, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(newValue, GetPropertyValue<T>(propertyName)))
{
return false;
}
propertyBackingDictionary[propertyName] = newValue;
OnPropertyChanged(propertyName);
Validate();
return true;
}
private ConcurrentDictionary<string, List<string>> errors = new ConcurrentDictionary<string, List<string>>();
public IValidator ModelValidator { get; set; }
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public void OnErrorsChanged(string propertyName)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
public IEnumerable GetErrors([CallerMemberName] string propertyName = null)
{
errors.TryGetValue(propertyName, out List<string> errorsForName);
return errorsForName;
}
public bool HasErrors => errors.Count > 0;
public void Validate()
{
errors.Clear();
var validationResults = ModelValidator.Validate(this);
foreach (var item in validationResults.Errors)
{
errors.TryAdd(item.PropertyName, new List<string> { item.ErrorMessage });
OnErrorsChanged(item.PropertyName);
}
}
}
}
Totally i am new to WPF, I need to solve the problem. Could anybody give me a sample xaml code without using code-behind.
Based on the questType (Check,radio, combo) Control need to be created based on the ObserverableCollection.
public class Order
{
public int OrderCode {get;set;}
public string Description {get;set;}
public ObserverableCollection<question> Questions{get;set;}
}
public class question
{
public string questType {get;set;}
public string Question {get;set;}
public ObserverableCollection<Answer> Answers {get;set;}
}
public class Answer
{
public string Ans{get; set;}
}
Based on the questType (Check,radio, combo)
Control need to be created based on the ObserverableCollection.
example:
1001 Pencil Gender? oMale oFemale oOther
[]Checkbox1 []Checkbox2
1002 Pen Fasting? oYes oNo
Here is how I would do it:
Code behind:
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Windows;
namespace Ans
{
public class Order
{
public int OrderCode { get; set; }
public string Description { get; set; }
public ObservableCollection<Question> Questions { get; set; }
}
public class Question
{
public string questType { get; set; }
public string Label { get; set; }
public ObservableCollection<Answer> Answers { get; set; }
}
public class Answer
{
public string Ans { get; set; }
public bool IsSelected { get; set; }
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
#region Order
/// <summary>
/// Order Dependency Property
/// </summary>
public static readonly DependencyProperty OrderProperty =
DependencyProperty.Register("Order", typeof(Order), typeof(MainWindow),
new FrameworkPropertyMetadata((Order)null));
/// <summary>
/// Gets or sets the Order property. This dependency property
/// indicates ....
/// </summary>
public Order Order
{
get { return (Order)GetValue(OrderProperty); }
set { SetValue(OrderProperty, value); }
}
#endregion
public MainWindow()
{
InitializeComponent();
Order = new Order()
{
Questions = new ObservableCollection<Question>()
{
new Question()
{
questType = "Combo",
Label = "Combo",
Answers = new ObservableCollection<Answer>()
{
new Answer(){Ans = "Female"},
new Answer(){Ans = "Male"}
}
},
new Question()
{
questType = "Check",
Label = "Multi",
Answers = new ObservableCollection<Answer>()
{
new Answer(){Ans = "Female"},
new Answer(){Ans = "Male"}
}
},
new Question()
{
questType = "Radio",
Label = "Radio",
Answers = new ObservableCollection<Answer>()
{
new Answer(){Ans = "Female"},
new Answer(){Ans = "Male"}
}
}
}
};
DataContext = this;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
foreach(Question q in Order.Questions)
{
Console.WriteLine( q.Label + " : " + string.Join(", " , q.Answers.Where(a=>a.IsSelected).Select(a=>a.Ans)) );
}
}
}
}
XAML:
<Window x:Class="Ans.MainWindow"
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:Ans"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<DataTemplate x:Key="ComboQuestion">
<ComboBox ItemsSource="{Binding Answers}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Ans}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.ItemContainerStyle>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
</DataTemplate>
<DataTemplate x:Key="CheckQuestion">
<ItemsControl ItemsSource="{Binding Answers}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Ans}" IsChecked="{Binding IsSelected, Mode=TwoWay}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
<DataTemplate x:Key="RadioQuestion">
<ItemsControl ItemsSource="{Binding Answers}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton Content="{Binding Ans}" IsChecked="{Binding IsSelected, Mode=TwoWay}" GroupName="{Binding DataContext.Label, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding Order.Questions}" Grid.IsSharedSizeScope="True">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Label"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Label}"/>
<ContentControl x:Name="ccQuestion" Grid.Column="1" Content="{Binding}" Margin="10"/>
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding questType}" Value="Combo">
<Setter TargetName="ccQuestion" Property="ContentTemplate" Value="{StaticResource ComboQuestion}"/>
</DataTrigger>
<DataTrigger Binding="{Binding questType}" Value="Check">
<Setter TargetName="ccQuestion" Property="ContentTemplate" Value="{StaticResource CheckQuestion}"/>
</DataTrigger>
<DataTrigger Binding="{Binding questType}" Value="Radio">
<Setter TargetName="ccQuestion" Property="ContentTemplate" Value="{StaticResource RadioQuestion}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="Order" Click="Button_Click" VerticalAlignment="Bottom"/>
</Grid>
</Window>
The only thing I've added to your model is IsSelected property that allows to know if this answer was selected.
The other important thing is the Radios. Their GroupName property defines the scope. So if no GroupName is set then when clicking on a radio in one question it will unselect radio in another question. I used Question label in my solution however it only works if labels are unique.
Another point is that data triggers are OK if you have 3-5 question types and if thay are only based on the questionType. However for more complex scenarios you can look for ItemTemplateSelector. It allows to write C# code that will select the template based on each item in ItemsControl.
I have a login View after successful login it should open Menu View as it has different tabs.Also I want the tabs to open inside the Menu View itself and the view should be closed once the other view is opened.
I have refered the following links:
Changing the View for a ViewModel
and
switching views in MVVM wpf.
I have done something like this:
MainWindow.xaml
<Window x:Class="JGC_ngCMS_Win.MainWindow"
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:JGC_ngCMS_Win.View"
xmlns:VM="clr-namespace:JGC_ngCMS_Win.ViewModel"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<VM:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate x:Key="View1Template" DataType="{x:Type VM:LoginViewModel}">
<local:LoginView></local:LoginView>
</DataTemplate>
<DataTemplate x:Key="View2Template" DataType="{x:Type VM:MenuViewModel}">
<local:MenuView />
</DataTemplate>
<DataTemplate x:Key="View3Template" DataType="{x:Type VM:UserModuleMapViewModel}">
<local:UserModuleMapView />
</DataTemplate>
</Window.Resources>
<Grid>
<ContentPresenter Content="{Binding ViewModelsView.CurrentItem}" Grid.Row="1"/>
<ContentControl Content="{Binding }">
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Setter Property="ContentTemplate" Value="{StaticResource View1Template}" />
<Style.Triggers>
<DataTrigger Binding="{Binding SwitchView}" Value="1">
<Setter Property="ContentTemplate" Value="{StaticResource View2Template}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
<ContentControl Content="{Binding ViewModel}" />
</Grid>
MainWindowViewModel.cs
public class MainWindowViewModel : ViewModelBase
{
private ViewModelBase _currentViewModel;
readonly static LoginViewModel _loginViewModel = new LoginViewModel();
readonly static MenuViewModel _menuViewModel = new MenuViewModel();
readonly static UserModuleMapViewModel _usermodulemapViewModel = new UserModuleMapViewModel();
public ViewModelBase CurrentViewModel
{
get
{
return _currentViewModel;
}
set
{
if (_currentViewModel == value)
return;
_currentViewModel = value;
OnPropertyChanged("CurrentViewModel");
}
}
public ICommand FirstViewCommand { get; private set; }
public ICommand SecondViewCommand { get; private set; }
public MainWindowViewModel()
{
CurrentViewModel = MainWindowViewModel._menuViewModel;
FirstViewCommand = new RelayCommand(() => ExecuteFirstViewCommand());
SecondViewCommand = new RelayCommand(() => ExecuteSecondViewCommand());
//ViewModels = new ObservableCollection<ViewModelBase>()
// {
// new LoginViewModel(),
// new MenuViewModel()
// //new ViewModel3()
// };
//ViewModelsView = CollectionViewSource.GetDefaultView(ViewModels);
}
public void ExecuteFirstViewCommand()
{
CurrentViewModel = MainWindowViewModel._usermodulemapViewModel;
}
private void ExecuteSecondViewCommand()
{
CurrentViewModel = MainWindowViewModel._menuViewModel;
}
My First screen is Login View which is perfect but after successful login Menu View Should open.What mistake am I committing?
Here is one of the possible answers.
I tried to make it as simple as possible, avoiding Styles for example.
The XAML code (I created the LoginViewModel and the MenuViewModel as UserControls to test it)
<Window x:Class="JGC_ngCMS_Win.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:JGC_ngCMS_Win.View"
xmlns:VM="clr-namespace:JGC_ngCMS_Win.ViewModel"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ContentPresenter Content="{Binding CurrentViewModel}">
<ContentPresenter.Resources>
<DataTemplate DataType="{x:Type VM:LoginViewModel}">
<local:LoginView/>
</DataTemplate>
<DataTemplate DataType="{x:Type VM:MenuViewModel}">
<local:MenuView />
</DataTemplate>
</ContentPresenter.Resources>
</ContentPresenter>
</Grid>
</Window>
Here is the minimum testable code for the ModelView classes
class ViewModelBase:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
class MainWindowViewModel:ViewModelBase
{
readonly LoginViewModel _loginViewModel = new LoginViewModel();
readonly MenuViewModel _menuViewModel = new MenuViewModel();
private ViewModelBase _currentViewModel;
public ViewModelBase CurrentViewModel
{
get
{
return _currentViewModel;
}
set
{
if (_currentViewModel == value)
return;
_currentViewModel = value;
OnPropertyChanged("CurrentViewModel");
}
}
//Just for test
public void switchView()
{
if (CurrentViewModel == _loginViewModel) { CurrentViewModel = _menuViewModel; }
else { CurrentViewModel = _loginViewModel; }
}
}
class LoginViewModel:ViewModelBase
{
}
class MenuViewModel:ViewModelBase
{
}
Then you just need to specify the DataContext:
ViewModel.MainWindowViewModel mainVM = new ViewModel.MainWindowViewModel();
public MainWindow()
{
InitializeComponent();
this.DataContext = mainVM;
}
Remark: I used an explicit instance for the DataContext, but you can work with static properties if you want.
I'm trying to print 5 albums and for every album print all its' songs with a checkbox near every song. (I recommend you to see the image published below)
I tried to do than with an ItemsControl but I don't know how to do that so every ItemsControl will Bind another list (with a specific album's songs).
I made all the 5 albums within a for loop.
My problems are:
For every album how do I create an ItemsControl for its' specific songs'
list.
Every time I check a CheckBox it checks all the checkboxes in its'
row (all the other albums).
Here is the code of a single ItemsControl:
<ItemsControl Grid.Column="1" ItemsSource="{Binding}"
Grid.IsSharedSizeScope="True"
Margin="12 0 12 0">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type domain:SelectableViewModel}">
<Border x:Name="Border" Padding="8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="Checkerz" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<CheckBox VerticalAlignment="Center" IsChecked="{Binding IsSelected}"/>
<StackPanel Margin="8 0 0 0" Grid.Column="1">
<TextBlock FontWeight="Bold" Text="{Binding Name}" />
<TextBlock Text="{Binding Description}" />
</StackPanel>
</Grid>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource MaterialDesignSelection}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
This is an image of how it's looks like now. (look at the red squares):
Click here
Please help me :(
try the next solution:
For every album how do I create an ItemsControl for its' specific songs' list.
Udate #1 - Xaml code solution
<Window x:Class="ListBoxOflistboxes.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:listBoxOflistboxes="clr-namespace:ListBoxOflistboxes"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.Resources>
<DataTemplate x:Key="InnerItemsControl" DataType="{x:Type listBoxOflistboxes:AlbumViewModel}">
<ItemsControl Grid.Column="1" ItemsSource="{Binding Songs}"
Grid.IsSharedSizeScope="True"
Margin="12 0 12 0">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type listBoxOflistboxes:SelectableViewModel}">
<Border x:Name="Border" Padding="8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="Checkerz" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<CheckBox VerticalAlignment="Center" IsChecked="{Binding IsSelected}"/>
<StackPanel Margin="8 0 0 0" Grid.Column="1">
<TextBlock FontWeight="Bold" Text="{Binding Name}" />
<TextBlock Text="{Binding Description}" />
</StackPanel>
</Grid>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter TargetName="Border" Property="Background" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding IsSelected}" Value="False">
<Setter TargetName="Border" Property="Background" Value="Transparent" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
<DataTemplate x:Key="ListBoxItemDataTemplate" DataType="listBoxOflistboxes:AlbumViewModel">
<ContentControl Content="{Binding }" ContentTemplate="{StaticResource InnerItemsControl}"></ContentControl>
</DataTemplate>
</Grid.Resources>
<Grid.DataContext>
<listBoxOflistboxes:MainAlbumsViewModel/>
</Grid.DataContext>
<ListBox ItemsSource="{Binding Albums}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"></StackPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid Margin="1">
<Border x:Name="MouseOverBorder" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Background="LightGreen" BorderBrush="DimGray" BorderThickness="1.5" Visibility="Collapsed"/>
<Border x:Name="SelectedBorder" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Background="Green" BorderBrush="Black" BorderThickness="1.5" Visibility="Collapsed"/>
<ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{StaticResource ListBoxItemDataTemplate}" />
</Grid>
<ControlTemplate.Triggers>
<!--Uncomment these trigger in order to make the mouse over border to be visible-->
<!--<Trigger Property="ListBoxItem.IsMouseOver" Value="True">
<Setter TargetName="MouseOverBorder" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="ListBoxItem.IsMouseOver" Value="False">
<Setter TargetName="MouseOverBorder" Property="Visibility" Value="Collapsed"/>
</Trigger>-->
<Trigger Property="ListBoxItem.IsSelected" Value="True">
<Setter TargetName="SelectedBorder" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="ListBoxItem.IsSelected" Value="False">
<Setter TargetName="SelectedBorder" Property="Visibility" Value="Collapsed"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Grid>
Every time I check a CheckBox it checks all the checkboxes in its' row (all the other albums).
Managed in view-models using shared models
/// <summary>
/// main view model
/// </summary>
public class MainAlbumsViewModel:BaseObservableObject
{
private readonly ISelectionEventAggregator _selectionEventAggregator;
private readonly ISongsProvider _songsProvider;
private ObservableCollection<AlbumViewModel> _albums;
public MainAlbumsViewModel()
{
_selectionEventAggregator = new SelectionEventAggregator();
_songsProvider = new SongsProvider();
Albums = new ObservableCollection<AlbumViewModel>
{
new AlbumViewModel(_selectionEventAggregator, _songsProvider),
new AlbumViewModel(_selectionEventAggregator, _songsProvider),
new AlbumViewModel(_selectionEventAggregator, _songsProvider),
};
Albums.ToList().ForEach(model =>
{
_songsProvider.RegisterSongs(model, new List<SelectableViewModel>
{
new SelectableViewModel(_selectionEventAggregator){Name = "Song A", Description = "Description A"},
new SelectableViewModel(_selectionEventAggregator){Name = "Song B", Description = "Description B"},
new SelectableViewModel(_selectionEventAggregator){Name = "Song C", Description = "Description C"},
new SelectableViewModel(_selectionEventAggregator){Name = "Song D", Description = "Description D"},
});
});
}
public ObservableCollection<AlbumViewModel> Albums
{
get { return _albums; }
set
{
_albums = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// an album view-model
/// </summary>
public class AlbumViewModel:BaseObservableObject
{
public AlbumViewModel(ISelectionEventAggregator selectionEventAggregator, ISongsProvider songsProvider)
{
_selectionEventAgreggator = selectionEventAggregator;
_songsProvider = songsProvider;
//you should think about an unsubscribe mechanism
_selectionEventAgreggator.SelectionEventHandler += SelectionEventAgreggatorOnSelectionEventHandler;
}
private void SelectionEventAgreggatorOnSelectionEventHandler(object sender, SelectionEventArgs args)
{
var key = args.Key as SelectableViewModel;
if(key == null) return;
var existingSong = Songs.FirstOrDefault(model => model.Name.Equals(key.Name) && model.Description.Equals(key.Description));
if(existingSong == null) return;
existingSong.UpdateSelectionSilentely(args.IsSelected);
}
private ObservableCollection<SelectableViewModel> _songs;
private readonly ISelectionEventAggregator _selectionEventAgreggator;
private readonly ISongsProvider _songsProvider;
public ObservableCollection<SelectableViewModel> Songs
{
get { return _songs ?? (_songs = new ObservableCollection<SelectableViewModel>(_songsProvider.GetSongs(this))); }
}
}
/// <summary>
/// helps to provide songs
/// </summary>
public interface ISongsProvider
{
List<SelectableViewModel> GetSongs(object albumKey);
void RegisterSongs(object albumKey, IEnumerable<SelectableViewModel> songs);
}
class SongsProvider : ISongsProvider
{
private Dictionary<object, List<SelectableViewModel>> _albums = new Dictionary<object, List<SelectableViewModel>>();
public List<SelectableViewModel> GetSongs(object albumKey)
{
return _albums.ContainsKey(albumKey) == false ? null : _albums[albumKey];
}
public void RegisterSongs(object albumKey, IEnumerable<SelectableViewModel> songs)
{
if (_albums.ContainsKey(albumKey) == false)
{
if(songs == null) return;
_albums.Add(albumKey, songs.ToList());
}
else
{
if (songs == null)
{
_albums.Remove(albumKey);
return;
}
_albums[albumKey] = songs.ToList();
}
}
}
/// <summary>
/// a single song view-model
/// </summary>
public class SelectableViewModel:BaseObservableObject
{
private readonly ISelectionEventAggregator _selectionEventAggregator;
private bool _isSelected;
private string _name;
private string _description;
public SelectableViewModel(ISelectionEventAggregator selectionEventAggregator)
{
_selectionEventAggregator = selectionEventAggregator;
}
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
OnPropertyChanged();
_selectionEventAggregator.Publish(new SelectionEventArgs {Key = this, IsSelected = _isSelected});
}
}
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged();
}
}
public string Description
{
get { return _description; }
set
{
_description = value;
OnPropertyChanged();
}
}
public void UpdateSelectionSilentely(bool isSelected)
{
_isSelected = isSelected;
OnPropertyChanged(() => IsSelected);
}
}
/// <summary>
/// helps to manage selection
/// </summary>
public interface ISelectionEventAggregator
{
event EventHandler<SelectionEventArgs> SelectionEventHandler;
void Publish(SelectionEventArgs selectionEventArgs);
}
public class SelectionEventAggregator : ISelectionEventAggregator
{
public event EventHandler<SelectionEventArgs> SelectionEventHandler;
public void Publish(SelectionEventArgs selectionEventArgs)
{
OnSelectionEventHandler(selectionEventArgs);
}
protected virtual void OnSelectionEventHandler(SelectionEventArgs e)
{
var handler = SelectionEventHandler;
if (handler != null) handler(this, e);
}
}
public class SelectionEventArgs:EventArgs
{
public object Key { get; set; }
public bool IsSelected { get; set; }
}
BaseObservableObject - simple INPC
/// <summary>
/// implements the INotifyPropertyChanged (.net 4.5)
/// </summary>
public class BaseObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged<T>(Expression<Func<T>> raiser)
{
var propName = ((MemberExpression)raiser.Body).Member.Name;
OnPropertyChanged(propName);
}
protected bool Set<T>(ref T field, T value, [CallerMemberName] string name = null)
{
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
OnPropertyChanged(name);
return true;
}
return false;
}
}
Explanation:
The main idea for the second part of your question is to use some kind of an event aggregation.
How it looks like
Regards.
I copied an example about dock panel from "WPF 4.5 Unleashed" ch.5. In the example the dock is set to be in the right of the grid So when in the code behind layer0.ColumnDefinitions.Add(column1CloneForLayer0); called, a column with content column1CloneForLayer0 will be created in the right side of the grid.
In my project I almost did the same thing, but when I call the function to add a column with some content, some empty space's created at the right side.. which is not as expected.So what's the right thing to do?
1.How to Add a column in the left side of a grid?
2.In the example the column was created yet why it's empty? Maybe Do i need to set the Z Index?
Update
I added something similar in the original example in the book and get wrong result. Below is the the source codes. I just added a leftToolBox in the left.
https://www.dropbox.com/sh/61hm139j77kz9k1/AACKqhG5uXFkQgnt8fWi4NvNa?dl=0
Update2
Relevant codes: In this case I just add a stackpanel with button in the left side and and click to call DocakPane(3), instead of giving the new column in the left, it creates column in the right.
public void DockPane(int paneNumber)
{
if (paneNumber == 1)
{
pane1Button.Visibility = Visibility.Collapsed;
pane1PinImage.Source = new BitmapImage(new Uri("pin.gif", UriKind.Relative));
// Add the cloned column to layer 0:
layer0.ColumnDefinitions.Add(column1CloneForLayer0);
// Add the cloned column to layer 1, but only if pane 2 is docked:
if (pane2Button.Visibility == Visibility.Collapsed) layer1.ColumnDefinitions.Add(column2CloneForLayer1);
}
else if (paneNumber == 2)
{
pane2Button.Visibility = Visibility.Collapsed;
pane2PinImage.Source = new BitmapImage(new Uri("pin.gif", UriKind.Relative));
// Add the cloned column to layer 0:
layer0.ColumnDefinitions.Add(column2CloneForLayer0);
// Add the cloned column to layer 1, but only if pane 1 is docked:
if (pane1Button.Visibility == Visibility.Collapsed) layer1.ColumnDefinitions.Add(column2CloneForLayer1);
}
else
{
leftpane1Button.Visibility = Visibility.Collapsed;
pane3PinImage.Source = new BitmapImage(new Uri("pin.gif", UriKind.Relative));
layer0.ColumnDefinitions.Add(testcol);
}
}
Based on the sample project provided, here is a re-write of the same using MVVM and most of the problem related to hard-coding would are gone. It is not pure MVVM but it is mostly MVVM to get started with.
start by defining ViewModels
a class to represent the dock pane
class DockablePaneVM : ViewModelBase
{
public DockablePaneVM(DockHostVM host)
{
Host = host;
}
private string _title;
public string Title
{
get { return _title; }
set
{
_title = value;
RaisePropertyChanged("Title");
}
}
private bool _isPinned;
public bool IsPinned
{
get { return _isPinned; }
set
{
_isPinned = value;
Host.PinModeChanged(this);
RaisePropertyChanged("IsPinned");
}
}
private object _content;
public object Content
{
get { return _content; }
set
{
_content = value;
RaisePropertyChanged("Content");
}
}
public DockHostVM Host { get; private set; }
public Dock Dock { get; set; }
}
host for the dockpanes, I have used collectionview for simplifying the location
class DockHostVM : ViewModelBase
{
public DockHostVM()
{
Panes = new ObservableCollection<DockablePaneVM>();
LeftPanes = new CollectionViewSource() { Source = Panes }.View;
RightPanes = new CollectionViewSource() { Source = Panes }.View;
FlotingLeftPanes = new CollectionViewSource() { Source = Panes }.View;
FlotingRightPanes = new CollectionViewSource() { Source = Panes }.View;
LeftPanes.Filter = o => Filter(o, Dock.Left, true);
RightPanes.Filter = o => Filter(o, Dock.Right, true);
FlotingLeftPanes.Filter = o => Filter(o, Dock.Left, false);
FlotingRightPanes.Filter = o => Filter(o, Dock.Right, false);
}
private bool Filter(object obj, Dock dock, bool isPinned)
{
DockablePaneVM vm = obj as DockablePaneVM;
return vm.Dock == dock && vm.IsPinned == isPinned;
}
public ObservableCollection<DockablePaneVM> Panes { get; set; }
public ICollectionView LeftPanes { get; set; }
public ICollectionView RightPanes { get; set; }
public ICollectionView FlotingLeftPanes { get; set; }
public ICollectionView FlotingRightPanes { get; set; }
private object _content;
public object Content
{
get { return _content; }
set { _content = value; }
}
public void PinModeChanged(DockablePaneVM paneVM)
{
LeftPanes.Refresh();
RightPanes.Refresh();
FlotingLeftPanes.Refresh();
FlotingRightPanes.Refresh();
}
//sample generator
public static DockHostVM GetSample()
{
DockHostVM vm = new DockHostVM();
vm.Panes.Add(new DockablePaneVM(vm) { Title = "Left Toolbox", Content = new ToolBoxVM() });
vm.Panes.Add(new DockablePaneVM(vm) { Title = "Solution Explorer", Content = new SolutionExplorerVM(), Dock = Dock.Right });
vm.Panes.Add(new DockablePaneVM(vm) { Title = "Toolbox", Content = new ToolBoxVM(), Dock = Dock.Right });
return vm;
}
}
then styles, to give provide the view for the classes
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:VisualStudioLikePanes">
<DataTemplate DataType="{x:Type l:DockablePaneVM}">
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<TextBlock Padding="8,4"
Text="{Binding Title}"
TextTrimming="CharacterEllipsis"
Background="{DynamicResource {x:Static SystemColors.ActiveCaptionBrushKey}}"
Foreground="{DynamicResource {x:Static SystemColors.ActiveCaptionTextBrushKey}}" />
<ToggleButton IsChecked="{Binding IsPinned}"
Grid.Column="1">
<Image Name="pinImage"
Source="pinHorizontal.gif" />
</ToggleButton>
<ContentControl Content="{Binding Content}"
Grid.Row="1"
Grid.ColumnSpan="2" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsPinned}"
Value="True">
<Setter Property="Source"
TargetName="pinImage"
Value="pin.gif" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
<Style TargetType="TabItem">
<Setter Property="Header"
Value="{Binding Title}" />
</Style>
<Style TargetType="TabItem"
x:Key="FloterItem"
BasedOn="{StaticResource {x:Type TabItem}}">
<Setter Property="LayoutTransform">
<Setter.Value>
<RotateTransform Angle="90" />
</Setter.Value>
</Setter>
</Style>
<Style TargetType="TabControl">
<Setter Property="l:TabControlHelper.IsLastItemSelected"
Value="True" />
<Style.Triggers>
<Trigger Property="HasItems"
Value="false">
<Setter Property="Visibility"
Value="Collapsed" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="TabControl"
x:Key="AutoResizePane"
BasedOn="{StaticResource {x:Type TabControl}}">
<Style.Triggers>
<Trigger Property="IsMouseOver"
Value="false">
<Setter Property="Width"
Value="23" />
</Trigger>
</Style.Triggers>
</Style>
<DataTemplate DataType="{x:Type l:ToolBoxVM}">
<ListBox Padding="10"
Grid.Row="1">
<ListBoxItem>Button</ListBoxItem>
<ListBoxItem>CheckBox</ListBoxItem>
<ListBoxItem>ComboBox</ListBoxItem>
<ListBoxItem>Label</ListBoxItem>
<ListBoxItem>ListBox</ListBoxItem>
</ListBox>
</DataTemplate>
<DataTemplate DataType="{x:Type l:SolutionExplorerVM}">
<TreeView Grid.Row="2">
<TreeViewItem Header="My Solution" IsExpanded="True">
<TreeViewItem Header="Project #1" />
<TreeViewItem Header="Project #2" />
<TreeViewItem Header="Project #3" />
</TreeViewItem>
</TreeView>
</DataTemplate>
</ResourceDictionary>
I have also created some dummy classes to represent the toolbox and solution explorer
also a helper class to improve the usability of tab control which i have used to host the dockpanes
class TabControlHelper
{
public static bool GetIsLastItemSelected(DependencyObject obj)
{
return (bool)obj.GetValue(IsLastItemSelectedProperty);
}
public static void SetIsLastItemSelected(DependencyObject obj, bool value)
{
obj.SetValue(IsLastItemSelectedProperty, value);
}
// Using a DependencyProperty as the backing store for IsLastItemSelected. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsLastItemSelectedProperty =
DependencyProperty.RegisterAttached("IsLastItemSelected", typeof(bool), typeof(TabControlHelper), new PropertyMetadata(false, OnIsLastItemSelected));
private static void OnIsLastItemSelected(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TabControl tc = d as TabControl;
tc.Items.CurrentChanged += (ss, ee) =>
{
if (tc.SelectedIndex < 0 && tc.Items.Count > 0)
tc.SelectedIndex = 0;
};
}
}
this will keep an item selected any time, in this project it will be used when a dock pane is pinned/unpinned
now the main window, note that I have bounded the dock Panes to 4 tab controls, left, right, left float & right float
<Window x:Class="VisualStudioLikePanes.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Height="500"
Width="800"
WindowStartupLocation="CenterScreen"
xmlns:l="clr-namespace:VisualStudioLikePanes">
<Window.DataContext>
<ObjectDataProvider MethodName="GetSample"
ObjectType="{x:Type l:DockHostVM}" />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Menu>
<MenuItem Header="FILE" />
...
<MenuItem Header="HELP" />
</Menu>
<DockPanel LastChildFill="True"
Grid.Row="1">
<Border Width="23"
DockPanel.Dock="Left"
Visibility="{Binding Visibility, ElementName=LeftFloter}" />
<Border Width="23"
DockPanel.Dock="Right"
Visibility="{Binding Visibility, ElementName=RightFloter}" />
<TabControl ItemsSource="{Binding LeftPanes}"
DockPanel.Dock="Left" />
<TabControl ItemsSource="{Binding RightPanes}"
DockPanel.Dock="Right" />
<Grid Name="layer0">
... page content
</Grid>
</DockPanel>
<TabControl ItemsSource="{Binding FlotingLeftPanes}"
Grid.Row="1"
HorizontalAlignment="Left"
TabStripPlacement="Left"
Style="{StaticResource AutoResizePane}"
ItemContainerStyle="{StaticResource FloterItem}"
x:Name="LeftFloter" />
<TabControl ItemsSource="{Binding FlotingRightPanes}"
Grid.Row="1"
HorizontalAlignment="Right"
TabStripPlacement="Right"
Style="{StaticResource AutoResizePane}"
ItemContainerStyle="{StaticResource FloterItem}"
x:Name="RightFloter" />
</Grid>
</Window>
result is your expected behavior with MVVM approach, adding new panel is easy as Panes.Add(new DockablePaneVM(vm) { Title = "Left Toolbox", Content = new ToolBoxVM() }); rest is handled.
Demo
download the working sample VisualStudioLikePanes.zip