I have two problems, one small and one big, and I need help;
First, this is my code :
<ComboBox Name="cmb1" Width="165" Height="25" Margin="25,5,10,10"
ItemsSource="{Binding ChoicesList}"
SelectedItem="{Binding SelectedChoiceList}">
</ComboBox>
<ComboBox Name="cmb2" Width="165" Height="25" Margin="10,5,25,10">
<ComboBox.Style>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="ItemsSource" Value="{Binding Sections}"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedChoiceList}" Value="Contract">
<Setter Property="ItemsSource" Value="{Binding Contract}"></Setter>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedChoiceList}" Value="Service">
<Setter Property="ItemsSource" Value="{Binding Services}"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
<ComboBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="{Binding Name}"
Margin="4,0"/>
<TextBlock Grid.Column="0"
Text="{Binding label}"
Margin="0"/>
<TextBlock Grid.Column="0"
Text="{Binding Start, StringFormat=d}"
Margin="0"/>
<TextBlock Grid.Column="1"
Text="{Binding End,StringFormat=d}"
Margin="0"/>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Another day, another problem!
In cmb2, I would like nothing to be loaded until I have made one of the three choices, is there a way?
(for the moment I load "section" list if nothing is chosen, and the binding changes according to the choices. I made this choice because the value of "section" can change, it can be "A" or "B" or "C" and more, so I would like "service" = service list, "contract" = contract list and "section"= all other values)
More importantly, I would like to rework this code because currently the DataTemplate is the same for all three binds, which was not awkward (If I choose "section", textblock displays "name" selected, cause section have no label, for example) but I would like, for contracts, to display "name" + "from" + "Starting date" + "To" + "Ending date".
If I add textblock "text = from" and "text = to", it will apply to all my choices, and I would have, for example, if I choose "section", a result like "nameOfSection + from + to", and I don't want that.
So I would like the textblock "from" and "to" to appear only for the choice "contract", I don't know if it's possible?
Hoping to have been clear, and thanking you for your time for the help you will give me;
Welcome to SO!
To answer your first point by removing the line that is setting the ItemSource you should be able to have nothing loaded
<Setter Property="ItemsSource" Value="{Binding Sections}"></Setter> can be removed and replaced with another DataTrigger.
To answer your second point you can use DataTemplates with the DataType property to apply a style to a specific data type. This means you can setup each of your, Service, Section and Contract to look differently / template their own properties.
<DataTemplate DataType="{x:Type classes:Contract}">
<Label Content="Contract"/>
</DataTemplate>
<DataTemplate DataType="{x:Type classes:Service}">
<Label Content="Service"/>
</DataTemplate>
<DataTemplate DataType="{x:Type classes:Section}">
<Label Content="Section"/>
</DataTemplate>
This bit of code demonstrates that.
Here is also a sample application I have made that should do what you've asked.
View
<Window x:Class="SOHelp.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:classes="clr-namespace:SOHelp.Classes"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<DataTemplate DataType="{x:Type classes:Contract}">
<Label Content="Contract"/>
</DataTemplate>
<DataTemplate DataType="{x:Type classes:Service}">
<Label Content="Service"/>
</DataTemplate>
<DataTemplate DataType="{x:Type classes:Section}">
<Label Content="Section"/>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<ComboBox Grid.Row="0" Width="165" Height="25"
ItemsSource="{Binding ChoicesList}"
DisplayMemberPath="Name"
SelectedItem="{Binding SelectedChoiceList}"/>
<ComboBox Grid.Row="1" Width="165" Height="25"
ItemsSource="{Binding SelectedChoiceList.Items}"/>
</Grid>
</Window>
Code behind / could be ViewModel
public partial class MainWindow : Window, INotifyPropertyChanged
{
private ObservableCollection<Option> _choicesList;
private Option _selectedChoiceList;
public Option SelectedChoiceList
{
get { return _selectedChoiceList; }
set { _selectedChoiceList = value; NotifyPropertyChanged(); }
}
public ObservableCollection<Option> ChoicesList
{
get { return _choicesList; }
set { _choicesList = value; NotifyPropertyChanged(); }
}
public MainWindow()
{
InitializeComponent();
ChoicesList = new ObservableCollection<Option>
{
new Option("Contract", new ObservableCollection<object>
{
new Contract(), new Contract(), new Contract()
}),
new Option("Service", new ObservableCollection<object>
{
new Service(), new Service(), new Service()
}),
new Option("Section", new ObservableCollection<object>
{
new Section(), new Section(), new Section()
}),
};
DataContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Classes I created
public class Option
{
public string Name { get; set; }
public ObservableCollection<object> Items { get; set; }
public Option(string name, ObservableCollection<object> items)
{
Name = name;
Items = items;
}
}
public class Service
{
}
public class Section
{
}
public class Contract
{
}
Hope some of this helps. Let me know if you have any questions.
Related
I have an ItemsControl to display translations text fields.
I want to setup validating, so if all translations are empty, there was an error, and fields was marked as "error".
Is there any possibilty to do this?
My xaml:
<ItemsControl x:Name="LanguageItemsControl" ItemsSource="{Binding Path=Translations, Mode=TwoWay}"
LostFocus="OnLostFocus" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="5,2,5,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="47*"/>
<ColumnDefinition Width="53*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" x:Name="ItemLabel" VerticalAlignment="Center"
Text="{Binding Path=Key, StringFormat={x:Static res:Resources.lblCaption}}" />
<TextBox Grid.Column="1" x:Name="ItemText" VerticalAlignment="Center"
HorizontalAlignment="Stretch" Margin="2,0,22,0"
Text="{Binding Path=Value, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=true, NotifyOnValidationError=true}"
LostFocus="OnLostFocus"
AcceptsReturn="True"
MaxLines="2"
ScrollViewer.VerticalScrollBarVisibility="Auto"
MaxLength="150">
</TextBox>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
My model's class implements from IDataErrorInfo and INotifyPropertyChanged
Translations is an ObservableCollection of custom type "LanguageValue" with public properties Key and Value.
I had in my model string this[string columnName], which works perfect with simple text boxes (outside ItemsControl), but how can make this works with my items? I've thight something like:
public string this[string columnName]
{
get
{
string result = null;
...
if (columnName == "Translations" || columnName == "ItemText")
{
if (Translations.All(t => string.IsNullOrEmpty(t.Value)))
result = Properties.Resources.errMsgEnterName;
}
...
But of course this didn't work.
Any suggestions?
Sure, I'm giving you a full implementation but with just the "Value" property. Do the same with all other properties that you want to validate:
1.Translation Model with IDataErrorInfo interface implementation:
public class Translation : BindableBase, IDataErrorInfo
{
public string Value { get; set; }
public string this[string propertyName]
{
get
{
return GetErrorForPropery(propertyName);
}
}
public string Error { get; }
private string GetErrorForPropery(string propertyName)
{
switch (propertyName)
{
case "Value":
if (string.IsNullOrEmpty(Value))
{
return "Please enter value";
}
return string.Empty;
default:
return string.Empty;
}
}
}
2.Initialize Translations in your ViewModel:
public ObservableCollection<Translation> Translations { get; set; }
public MainViewModel()
{
Translations = new ObservableCollection<Translation>
{
new Translation {Value = "A"},
new Translation (),
new Translation {Value = "C"}
};
}
3.Xaml with ValidatesOnDataErrors on the Value TextBox:
<ItemsControl x:Name="LanguageItemsControl" ItemsSource="{Binding Path=Translations, Mode=TwoWay}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="5,2,5,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="47*"/>
<ColumnDefinition Width="53*"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" x:Name="ItemLabel" VerticalAlignment="Center" Text="{Binding Path=Value, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
4.That will show a red box around the empty TextBox, if you want to display the error messeage when hovering overthe TextBox you need a tool tip:
<Window x:Class="WpfApplication1.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:customControlLib="clr-namespace:CustomControlLib;assembly=CustomControlLib"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
Title="MainWindow"
DataContext="{StaticResource MainViewModel}">
<Window.Resources>
<Style x:Key="TextBoxStyle" TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<ItemsControl x:Name="LanguageItemsControl" ItemsSource="{Binding Path=Translations, Mode=TwoWay}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="5,2,5,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="47*"/>
<ColumnDefinition Width="53*"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" x:Name="ItemLabel" Style="{StaticResource TextBoxStyle}" VerticalAlignment="Center" Text="{Binding Path=Value, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I'm trying to add buttons to UserControl. I have to follow MVVM pattern. I have created a class, DeviceButton, with different set/gets and a constructor. In a viewmodel, used as datacontext, is the ObservableCollection and a get-method for the collection. I have bound the collection to a ItemsControl source and tried to add a template. I guess I'm missing something 'cause the buttons won't load.
The UserControl is added into a tab (using dragablz) which as well is a part of a ObservableCollection, also added at run time (this is working just fine). The idea is, that the tab has a list of buttons that has to be created at run time, where the list is fetched from a web service - so the buttons has to be added dynamically/programatically. Overview is just the first tab - a template for each tab (reflecting the fetched items) is being implemented when the buttons work. For now, I'm just adding a test button to the collection, but as stated, this won't show. What am I missing?
I have a Overview.xaml file:
<UserControl // attributes omitted to save spaces... ask if needed>
<StackPanel>
<Border>
<TextBlock FontSize="16" FontWeight="Bold" TextWrapping="WrapWithOverflow"
TextAlignment="Center" HorizontalAlignment="Center"
Foreground="{DynamicResource AccentColorBrush}">
Welcome to Greenhouse App
</TextBlock>
</Border>
<ItemsControl ItemsSource="{Binding DeviceButtons}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vmodel:DeviceButton}">
<Button Width="50" Height="50" Margin="10 10 0 0" Command="{Binding OpenTab}"
CommandParameter="{Binding DeviceType}" HorizontalAlignment="Left"
VerticalAlignment="Top" Style="{DynamicResource SquareButtonStyle}">
<StackPanel>
<Image Source="{Binding ImageUrl}" />
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
// Manually added test button...
<Button x:Name="windMill" HorizontalAlignment="Left" Margin="10,0,0,0"
VerticalAlignment="Top" Width="50" Height="50"
Command="{Binding OpenTab}" FontFamily="Segoe Ui"
Style="{DynamicResource SquareButtonStyle}">
<StackPanel>
<Image Source="/Greenhouse;component/Icons/Windmill.png" />
</StackPanel>
</Button
</StackPanel>
I'm trying to add an ObservableCollection of type DeviceButton, the _deviceButtons collection (Binded as ItemsSource to the ItemsControl)
The tabList items are working just find, and I can manually add more if needed (these will later be added through the OpenNewTab-command, which should be bound to the buttons)
The DeviceButton file:
public class DeviceButton
{
private readonly string _content;
private readonly string _deviceType;
private readonly string _imageUrl;
public DeviceButton(string content, string deviceType, string imageUrl)
{
_content = content;
_deviceType = deviceType;
_imageUrl = imageUrl;
}
public string Content
{
get { return _content; }
}
public string DeviceType
{
get { return _deviceType; }
}
public string ImageUrl
{
get { return _imageUrl; }
}
}
The collection is located in a viewmodel file, MainWindowViewModel.cs:
public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
{
public ICommand OpenTab { get; set; }
private string tabControl { get; set; } = "0";
private IInterTabClient _interTabClient;
private ObservableCollection<TabContent> _tabContents = new ObservableCollection<TabContent>();
private ObservableCollection<DeviceButton> _deviceButtons = new ObservableCollection<DeviceButton>();
public MainWindowViewModel()
{
_interTabClient = new MyInterTabClient();
DeviceButtons.Add(new DeviceButton("Windturbine", "windturbine", "/Greenhouse;component/Icons/Windmill.png"));
TabContents.Add(new TabContent("Overview", new Overview()));
OpenTab = new RelayCommand<object>(OpenNewTab);
}
private void OpenNewTab(object obj)
{
MessageBox.Show("Yo"); // Not yet implemented
RaisePropertyChanged(() => tabControl);
}
public ObservableCollection<TabContent> TabContents
{
get { return _tabContents; }
}
public ObservableCollection<DeviceButton> DeviceButtons
{
get { return _deviceButtons; }
}
public IInterTabClient InterTabClient
{
get { return _interTabClient; }
}
}
The buttons aren't loaded when I start the program (only the "test" button added manually in the WPF).
The UserControl, Overview, is considered a tab in another Controls.Metrowindow, MainWindow.xaml:
<Window.Resources>
<Style TargetType="{x:Type dragablz:TabablzControl}">
<Setter Property="CustomHeaderItemTemplate">
<Setter.Value>
<DataTemplate DataType="{x:Type viewmodel:TabContent}">
<TextBlock Text="{Binding Header}" />
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate DataType="{x:Type viewmodel:TabContent}">
<ContentPresenter Margin="4" Content="{Binding Content}" />
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<dragablz:TabablzControl SelectedIndex="{Binding tabControl}" ItemsSource="{Binding TabContents}" x:Name="InitialTabablzControl" Margin="4 0 4 4">
<dragablz:TabablzControl.InterTabController>
<dragablz:InterTabController InterTabClient="{Binding MyInterTabClient}" />
</dragablz:TabablzControl.InterTabController>
</dragablz:TabablzControl>
I guess it's an issue with the resources/binding in the Overview.xaml, but I've exhausted all suggested solutions I could find.
Issue was in binding
ItemsSource="{Binding Path=DataContext.TabContents,
RelativeSource={RelativeSource AncestorLevel=1,
AncestorType={x:Type Window}, Mode=FindAncestor}}"
I suspect the DataContext is being set to a TabContent object, which does not have a DeviceButtons property
I would suggest using a tool like Snoop to verify your DataContext is what you expect.
I am developing a WPF application and in one window I used a wizard component from WPF toolkit. In this wizard I'm creating a new person. In second step I am using an enumeration as a source for possible contact types (for example Phone, Email...).
This is my wizard page in XAML:
<xctk:WizardPage x:Name="NewContactPage" PageType="Interior"
Title="Contacts" Style="{DynamicResource NewContactPage}"
CanCancel="True" CanFinish="False"
Loaded="NewContactPage_Loaded"
PreviousPage="{Binding ElementName=NewPersonPage}">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Top">
<control:DataLoader x:Name="ctrNewContactLoader" />
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Top" Orientation="Vertical">
<ItemsControl ItemsSource="{Binding Path=Person.PersonContacts, Mode=TwoWay,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType=Window}}"
Name="icContacts">
<ItemsControl.ItemTemplate>
<ItemContainerTemplate>
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Top" Orientation="Vertical"
Margin="5" Background="WhiteSmoke">
<CheckBox IsChecked="{Binding Path=IsValid}"
Content="{Binding Path=ContactType.Description}"
Name="cbContactVisible"/>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Top"
Visibility="{Binding ElementName=cbContactVisible, Path=IsChecked,
Converter={StaticResource BooleanToVisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<TextBox Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Stretch" MaxLength="64"
Name="txtContactValue"
Text="{Binding Path=Contact,
ValidatesOnDataErrors=True,
ValidatesOnNotifyDataErrors=True,
ValidatesOnExceptions=True}" />
</Grid>
</StackPanel>
</ItemContainerTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
The source of ItemsControl is a List of PersonContactModel class:
public class PersonContactModel : BaseObjectModel
{
public PersonContactModel()
{
this.Created = DateTime.Now;
this.Updated = DateTime.Now;
this.IsValid = true;
this.ContactType = new ContactTypeModel();
}
public string Contact { get; set; }
public ContactTypeModel ContactType { get; set; }
public DateTime Created { get; set; }
public int Id { get; set; }
public bool IsValid { get; set; }
public DateTime Updated { get; set; }
public override string this[string columnName]
{
get
{
string retVal = string.Empty;
switch (columnName)
{
case "Contact":
retVal = base.Concat(base.RequeiredField(this.Contact), base.MinLength(this.Contact, 5), base.MaxLength(this.Contact, 62));
break;
}
return retVal;
}
}
}
the base class implement a IDataErrorInfo interface with validation info about Contact property.
The desired behavior is that if the checkbox is checked, it is visible grid with a field for entering a contact, otherwise not. Button next step should be seen only when selected contact types are valid. This functionality is trying to accomplish the following styles in app.xaml:
<Style TargetType="xctk:WizardPage" x:Key="NewContactPage">
<Setter Property="NextButtonVisibility" Value="Hidden" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=(Validation.HasError), ElementName=txtContactValue}" Value="False" />
</MultiDataTrigger.Conditions>
<Setter Property="NextButtonVisibility" Value="Visible" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
Unfortunately, the button for the next step is invisible, even if it asks all kinds of contact for the new person and will fulfill all the conditions for a valid entry.
What's wrong? Where is an error?
You are trying to achieve what you want in a not very good way. Error in this particular code is because you reference element "txtContactValue" from your style trigger, and style has no idea at all what this element is. By the way, if you look at output window when debugging your code, I bet you will see this error there.
Now, even if you will try to reference "txtContactValue" without style, like this:
NextButtonVisibility="{Binding ElementName=txtContactValue, Path=(Validation.HasError), Converter={StaticResource BooleanToVisibilitConverter}}"
It won't work, because txtContactValue is in different scope. BUT you should not do this in a first place! You have a model for your data, and that is model which controls if data is valid or not. Just add some property to your model which indicates if data which you create on this wizard page is valid (like PersonContact.IsValid) and you can proceed to the next page, and bind to this property.
I am new with WPF - I want to create a tester for my server
I want to have on the right side of the application a TreeView and whenever a user selects a node - appropriate items is shown on the right side. For example I have a Connection node and under it many Sessions nodes, The Connection and Session have different parameters. I have built the tree view using mvvm and all works fine, but how can I achieve the second goal?
the xaml
<Window x:Class="Tree1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:self ="clr-namespace:Tree1"
xmlns:models ="clr-namespace:Tree1.Models"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<HierarchicalDataTemplate DataType="{x:Type self:TestPlanViewModel}" ItemsSource="{Binding Connections}">
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type self:ConnectionViewModel}" ItemsSource="{Binding Sessions}">
<StackPanel>
<TextBlock Text="Connection" Margin="10, 0, 0,0"></TextBlock>
</StackPanel>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type self:SessionViewModel}">
<StackPanel Orientation="Horizontal">
<TextBlock Margin="10,0,0,0" Text="Session"></TextBlock>
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid Margin="10">
<Button Height="23" VerticalAlignment="Top" Margin="277,10,144,0" Name="addSessionBtn" Width="76"></Button>
<TreeView Name="testPlanTview" Margin="0,10,283,0" SelectedItemChanged="testPlanTview_SelectedItemChanged">
<TreeViewItem ItemsSource="{Binding Connections}" Header="Test Plan">
</TreeViewItem>
</TreeView>
</Grid>
You can use DataTemplates. There are two possible solution for your problem.
First of all let's create a base class:
public abstract class SelectableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool isSelected;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public bool IsSelected
{
get
{
return isSelected;
}
set
{
if (isSelected != value)
{
isSelected = value;
OnPropertyChanged("IsSelected");
}
}
}
}
For our example we have two classes:
Car, whose properties are "Hp" (int) and "Matriculation" (DateTime)
Person, whose properties are "Name" (string) and "Surname" (string)
Both of them extend SelectableObject.
Now the ViewModel (of course it is just a sample):
public class ViewModel : SelectableObject
{
private ArrayList tree = new ArrayList();
private ObjectWrapper current;
private Car car = new Car();
private Person person = new Person();
public ViewModel()
{
car.Hp = 120;
car.Matriculation = DateTime.Today;
car.PropertyChanged += new PropertyChangedEventHandler(OnItemPropertyChanged);
person.Name = "John";
person.Surname = "Doe";
person.PropertyChanged += new PropertyChangedEventHandler(OnItemPropertyChanged);
tree.Add(car);
tree.Add(person);
}
private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
SelectableObject impl = (SelectableObject)sender;
if (e.PropertyName == "IsSelected" && impl.IsSelected)
{
Current = new ObjectWrapper(impl);
}
}
public ObjectWrapper Current
{
get
{
return current;
}
private set
{
current = value;
OnPropertyChanged("Current");
}
}
public IEnumerable Tree
{
get
{
return tree;
}
}
}
The ViewModel uses a the ObjectWrapper class:
public class ObjectWrapper
{
private readonly object wrappedInstance;
private readonly ReadOnlyCollection<PropertyWrapper> propertyWrappers;
public ObjectWrapper(object instance)
{
Collection<PropertyWrapper> collection = new Collection<PropertyWrapper>();
Type instanceType;
wrappedInstance = instance;
instanceType = instance.GetType();
foreach (PropertyInfo propertyInfo in instanceType.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance))
{
collection.Add(new PropertyWrapper(instance, propertyInfo));
}
propertyWrappers = new ReadOnlyCollection<PropertyWrapper>(collection);
}
public ReadOnlyCollection<PropertyWrapper> PropertyWrappers
{
get
{
return propertyWrappers;
}
}
public object Instance { get { return wrappedInstance; } }
}
public class PropertyWrapper
{
private readonly object instance;
private readonly PropertyInfo propertyInfo;
public PropertyWrapper(object instance, PropertyInfo propertyInfo)
{
this.instance = instance;
this.propertyInfo = propertyInfo;
}
public string Label
{
get
{
return propertyInfo.Name;
}
}
public Type PropertyType
{
get
{
return propertyInfo.PropertyType;
}
}
public object Value
{
get
{
return propertyInfo.GetValue(instance, null);
}
set
{
propertyInfo.SetValue(instance, value, null);
}
}
}
First solution (the easiest one)
You can use implicit datatemplating:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
xmlns:toolkit="http://schemas.xceed.com/wpf/xaml/toolkit"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
Title="MainWindow" Height="350" Width="600">
<DockPanel>
<TreeView ItemsSource="{Binding Tree}" DockPanel.Dock="Left" Margin="5">
<TreeView.ItemContainerStyle>
<Style BasedOn="{StaticResource {x:Type TreeViewItem}}" TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
<ContentControl Content="{Binding Path=Current.Instance, Mode=OneWay}" Margin="5">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type local:Car}">
... define your car template here ...
</DataTemplate>
<DataTemplate DataType="{x:Type local:Person}">
... define your person template here ...
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
</DockPanel>
</Window>
Second solution (imho the best one)
You can take advantage of ObjectWrapper object, by using an ItemsControl (here I used Extended WPF Toolkit for DateTime and Int controls):
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
xmlns:toolkit="http://schemas.xceed.com/wpf/xaml/toolkit"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
Title="MainWindow" Height="350" Width="600">
<Window.Resources>
<local:ItemTemplateSelector x:Key="ItemTemplateSelector" />
<DataTemplate x:Key="{x:Type sys:String}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Label, Mode=OneWay}" VerticalAlignment="Center" />
<TextBox Text="{Binding Path=Value, Mode=TwoWay}" Margin="5" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="{x:Type sys:DateTime}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Label, Mode=OneWay}" VerticalAlignment="Center" />
<toolkit:DateTimePicker Value="{Binding Path=Value, Mode=TwoWay}" Margin="5" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="{x:Type sys:Int32}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Label, Mode=OneWay}" VerticalAlignment="Center" />
<toolkit:IntegerUpDown Value="{Binding Path=Value, Mode=TwoWay}" Margin="5" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<DockPanel>
<TreeView ItemsSource="{Binding Tree}" DockPanel.Dock="Left" Margin="5">
<TreeView.ItemContainerStyle>
<Style BasedOn="{StaticResource {x:Type TreeViewItem}}" TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
<ItemsControl ItemsSource="{Binding Path=Current.PropertyWrappers, Mode=OneWay}"
Margin="5" ItemTemplateSelector="{StaticResource ItemTemplateSelector}" />
</DockPanel>
</Window>
The implementation of the ItemTemplateSelector it is not difficult:
public class ItemTemplateSelector : DataTemplateSelector
{
public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
{
PropertyWrapper propertyWrapper = (PropertyWrapper)item;
FrameworkElement frameworkElement = (FrameworkElement)container;
DataTemplate dataTemplate = (DataTemplate)frameworkElement.TryFindResource(propertyWrapper.PropertyType);
return dataTemplate;
}
}
My answer is quite long, but I wanted to show you both roads you can use.
Of course you can improve the PropertyWrapper by using attributes.
If you're already using MVVM i would sugest something like System.Windows.Interactivity and Prism regions to implement what you need.
For example:
Xaml:
<TreeView Name="testPlanTview" Margin="0,10,283,0">
<TreeViewItem ItemsSource="{Binding Connections}" Header="Test Plan">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<i:InvokeCommandAction Command="{Binding OpenNewViewCommand}"
CommandParameter="{Binding SelectedItem,ElementName=testPlanTview}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TreeViewItem>
</TreeView>
<ContentControl prism:RegionManager.RegionName="MyRegion"/>
Viewmodel:
public ICommand OpenNewViewCommand
{
get { return this.selectedCommand; }
}
In the view model constructor you add:
this.selectedCommand = new DelegateCommand<YourModel>(this.SelectedExecute);
And the command:
private void SelectedExecute(YourModel parameter)
{
this.regionManager.RequestNavigate(RegionNames.MyRegion, new Uri("ViewToNavigate", UriKind.Relative), parameters);
}
Please be aware that this is just an example on how to make the navigation possible with prism. For more information on what i'm suggesting you can check the msdn link here
I made a project based on nested tabs.
the nested tabs are different instance of the same viemModel and the same UI.
when I switch between the tabs he comboboxes present in the tabs chenge thei selection depending on the tab that is loosing focus.
I add both the viewmodels and the view of my test project.
thank you in advance for your help
main window
<Window.Resources>
<DataTemplate DataType="{x:Type local:IntermediateViewModel}">
<local:IntermediateView />
</DataTemplate>
<DataTemplate x:Key="HeaderedTabItemTemplate">
<Grid>
<ContentPresenter
Content="{Binding Path=Header, UpdateSourceTrigger=PropertyChanged}"
VerticalAlignment="Center" >
</ContentPresenter>
</Grid>
</DataTemplate>
<Style x:Key="SimpleTabItemStyle" TargetType="TabItem">
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Grid>
<Border Name="Border" BorderThickness="1" BorderBrush="#555959">
<ContentPresenter x:Name="ContentSite" VerticalAlignment="Center" HorizontalAlignment="Center"
ContentSource="Header" Margin="12,2,12,2" RecognizesAccessKey="True" Height ="40" MinWidth ="90"/>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="Background" Value="#555959" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<DataTemplate x:Key="DefaultTabControlTemplate">
<TabControl IsSynchronizedWithCurrentItem="True"
BorderThickness="0"
ItemsSource="{Binding}"
ItemTemplate="{StaticResource HeaderedTabItemTemplate}"
ItemContainerStyle="{StaticResource SimpleTabItemStyle}"
SelectionChanged="TabControl_SelectionChanged"
/>
</DataTemplate>
<!---->
</Window.Resources>
<Grid MinHeight="200" MinWidth="300">
<Grid.RowDefinitions>
<RowDefinition Height="260*" />
<RowDefinition Height="51*" />
</Grid.RowDefinitions>
<Border >
<ContentControl
Content="{Binding Path=Workspaces}"
ContentTemplate="{DynamicResource DefaultTabControlTemplate}"
/>
</Border>
<Button Grid.Row="1" Content="Add" Command="{Binding AddCommand}"/>
</Grid>
view model (create a different istance each time)
class MainWindowViewModel : WorkspacesViewModel<IntermediateViewModel>
{
public MainWindowViewModel()
{
this.WorkspacesView.CurrentChanged += new EventHandler(WorkspacesView_CurrentChanged);
}
void WorkspacesView_CurrentChanged(object sender, EventArgs e)
{
}
RelayCommand myVar = null;
public ICommand AddCommand
{
get
{
return myVar ?? (myVar = new RelayCommand(param =>
{
SetWindow(new IntermediateViewModel("AA" + this.Workspaces.Count) );
}));
}
}
first level tab
<UserControl.Resources>
<DataTemplate DataType="{x:Type local:ClassViewModel}">
<local:ClassView />
</DataTemplate>
</UserControl.Resources>
<Border>
<ContentControl Content="{Binding Path=CurrentWorkspace, Mode=OneWay}" Loaded="ContentControl_Loaded" DataContextChanged="ContentControl_DataContextChanged" IsVisibleChanged="ContentControl_IsVisibleChanged" LayoutUpdated="ContentControl_LayoutUpdated" TargetUpdated="ContentControl_TargetUpdated" Unloaded="ContentControl_Unloaded" />
</Border>
first level viewmodel
class IntermediateViewModel : WorkspacesViewModel
{
public string Header { get; set; }
public IntermediateViewModel(string header)
{
Header = header;
SetWindow(new ClassViewModel(header));
}
}
nested tab
<UserControl.Resources>
<CollectionViewSource x:Key="StatusView" Source="{Binding Path=StatusList}"/>
</UserControl.Resources>
<Grid>
<ComboBox Name="_spl2Status" ItemsSource="{Binding Source={StaticResource StatusView}}"
SelectedValue="{Binding Path=MyProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="FL_TYPE"
DisplayMemberPath="ID_TYPE" Margin="76,12,0,0" Height="40" VerticalAlignment="Top" HorizontalAlignment="Left" Width="146"
DataContextChanged="_spl2Status_DataContextChanged"
IsVisibleChanged="_spl2Status_IsVisibleChanged"
Loaded="_spl2Status_Loaded"
SelectionChanged="_spl2Status_SelectionChanged"
>
</ComboBox>
</Grid>
nested tab view model
public enum myTypes
{
tipo0 = 0,
tipo1 = 1,
tipo2 = 2,
}
class ClassViewModel : WorkspaceViewModel
{
public ClassViewModel(string name)
{
Name = name;
}
public string Name { get; set; }
private List<IntEnumType> _statusList = null;
public List<IntEnumType> StatusList
{
get
{
if (_statusList == null)
_statusList = new List<IntEnumType>()
{
new IntEnumType((int)myTypes.tipo0, myTypes.tipo0.ToString()),
new IntEnumType((int)myTypes.tipo1, myTypes.tipo1.ToString()),
new IntEnumType((int)myTypes.tipo2, myTypes.tipo2.ToString()),
};
return _statusList;
}
}
private int myVar = 1;
public int MyProperty
{
get
{
return myVar;
}
set
{
if (myVar != value)
{
myVar = value;
OnPropertyChanged(() => MyProperty);
}
}
}
}
public class TabItemStyleSelector : StyleSelector
{
public Style MainTabItem { get; set; }
public Style ChildrenTabItem { get; set; }
public Style SpecificationTabItem { get; set; }
public override Style SelectStyle(object item, DependencyObject container)
{
//if (item is IHome)
// return MainTabItem;
//else if (item is SpecificationItemViewModel)
// return SpecificationTabItem;
//else
return ChildrenTabItem;
}
}
The code is a little hard to completely follow, but I'm guessing that the issue is that there is only one instance of your ClassViewModel and it is where the selection for the combo box is stored {Binding Path=MyProperty, so whatever is stored in MyProperty will be reflected in all instances of the combo box regardless of where they live.
Well this is a bit late, but as I'm facing the same issue, I want to share my analysis.
When you change your tabs, you change the DataContext of the current Tab to your other ViewModel and hence also the ItemsSource of your ComboBox.
In case your previously selected Item (SelectedItem) is not contained within the new ItemsSource, the ComboBox fires a SelectionChanged-Event and therefore sets the SelectedIndex to -1.
Altough this default behaviour of the ComboBox might make sense, it's very annoying in many cases.
We've derived an own class from ComboBox, handling that. But it's not very satisfying as you loose some default behaviour you most probably need.
The problem is in your loaded event handlers.
When you switch tabs your unloading one tab and loading a new one.
I suspect your changing MyComboBox.SelectedIndex in _spl2Status_Loaded.