I'm trying to make a custom TreeView and make it a user control. When I wrap the user control in another window, I tried to get the TreeView item double click event in the main window.
<Window xmlns:avalondock="http://avalondock.codeplex.com" x:Class="WellsVisualizationWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:well="clr-namespace:VisualizationWPF.ViewModel.ViewUserControl"
Title="e-IFD" Height="408" Width="558" WindowState="Maximized"
>
<Grid MinWidth="100" **TreeViewItem.MouseLeftButtonClick=<EventHandler>**> <-- Trying to override but failed :p
<local:CustomTreeView />
</Grid>
I tried to get the bubbling mouse double click from CustomTreeView item and intercept the event in the grid wrapper outside the usercontrol. I tried to add TreeViewItem. TreeViewItem.MouseLeftButtonDown="Grid_MouseLeftButtonDown and failed. Any ideas to solve my problem ?
Here is my code of custom user control for treeview
<UserControl x:Class="WellsVisualizationWPF.ViewModel.ViewUserControl.WellsTreeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:VisualizationWPF.ViewModel"
>
<Grid MinWidth="100">
<Grid.RowDefinitions>
<RowDefinition MaxHeight="500" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="1">
<TextBlock TextWrapping="Wrap" FontSize="12">
Text1
</TextBlock>
<Button Height="24" Content="Add New" Name="btn_add" Click="btn_add_Click" />
</StackPanel>
<ScrollViewer>
<DockPanel>
<TreeView Grid.Row="0" ItemsSource="{Binding Data}">
<TreeView.Resources>
<HierarchicalDataTemplate
DataType="{x:Type local:MainViewModel}"
ItemsSource="{Binding Children}"
>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
</StackPanel>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type local:ParamsViewModel}">
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</TreeView.Resources>
</TreeView>
</DockPanel>
</ScrollViewer>
</Grid>
You don't need to publish double-click event outside from the user control at all.
You need to add some InputBinding (MouseBinding in this particular case) into InputBindings collection of the TreeView.SelectedItem.
The problem is that you can't do that in normal, obvious way - set InputBindings via TreeView.ItemContainerStyle, because InputBindings collection is read-only. Sad, but true.
Good news is that you can use attached property to accomplish that.
The sample:
View models.
a) this is what will be displayed as items in tree view:
public class Node : ViewModelBase
{
public String Text
{
get { return text; }
set
{
if (text != value)
{
text = value;
OnPropertyChanged("Text");
}
}
}
private String text;
public ObservableCollection<Node> Nodes { get; set; }
}
b) this is "main" view model:
public class ViewModel : ViewModelBase
{
public ViewModel()
{
this.selectedNodeDoubleClickedCommand = new RelayCommand<Node>(node =>
{
Debug.WriteLine(String.Format("{0} clicked!", node.Text));
});
}
public ObservableCollection<Node> Nodes { get; set; }
public RelayCommand<Node> SelectedNodeDoubleClickedCommand
{
get { return selectedNodeDoubleClickedCommand; }
}
private readonly RelayCommand<Node> selectedNodeDoubleClickedCommand;
}
User control code-behind. Basic idea - we're adding one attached property to set input binding though it in XAML, and another one - to allow external world bind command, when input binding fires:
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
public ICommand SelectedItemDoubleClickedCommand
{
get { return (ICommand)GetValue(SelectedItemDoubleClickedCommandProperty); }
set { SetValue(SelectedItemDoubleClickedCommandProperty, value); }
}
public static readonly DependencyProperty SelectedItemDoubleClickedCommandProperty = DependencyProperty.Register(
"SelectedItemDoubleClickedCommand", typeof(ICommand),
typeof(UserControl1),
new UIPropertyMetadata(null));
public static ICommand GetSelectedItemDoubleClickedCommandAttached(DependencyObject obj)
{
return (ICommand)obj.GetValue(SelectedItemDoubleClickedCommandAttachedProperty);
}
public static void SetSelectedItemDoubleClickedCommandAttached(DependencyObject obj, ICommand value)
{
obj.SetValue(SelectedItemDoubleClickedCommandAttachedProperty, value);
}
public static readonly DependencyProperty SelectedItemDoubleClickedCommandAttachedProperty = DependencyProperty.RegisterAttached(
"SelectedItemDoubleClickedCommandAttached",
typeof(ICommand), typeof(UserControl1),
new UIPropertyMetadata(null, SelectedItemDoubleClickedCommandAttachedChanged));
private static void SelectedItemDoubleClickedCommandAttachedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var item = d as TreeViewItem;
if (item != null)
{
if (e.NewValue != null)
{
var binding = new MouseBinding((ICommand)e.NewValue, new MouseGesture(MouseAction.LeftDoubleClick));
BindingOperations.SetBinding(binding, InputBinding.CommandParameterProperty, new Binding("SelectedItem")
{
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(TreeView), 1)
});
item.InputBindings.Add(binding);
}
}
}
}
User control XAML:
<Grid>
<TreeView ItemsSource="{Binding Nodes}">
<TreeView.Resources>
<HierarchicalDataTemplate ItemsSource="{Binding Nodes}" DataType="{x:Type local:Node}">
<TextBlock Text="{Binding Text}"/>
</HierarchicalDataTemplate>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="local:UserControl1.SelectedItemDoubleClickedCommandAttached"
Value="{Binding SelectedItemDoubleClickedCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
</Grid>
Main window XAML:
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
Title="MainWindow" Height="350" Width="525">
<Grid>
<local:UserControl1 SelectedItemDoubleClickedCommand="{Binding SelectedNodeDoubleClickedCommand}"/>
</Grid>
</Window>
Main window code-behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel
{
Nodes = new ObservableCollection<Node>
{
new Node
{
Text = "Parent 1",
Nodes = new ObservableCollection<Node>
{
new Node { Text = "Child 1.1"},
new Node { Text = "Child 1.2"},
}
},
new Node
{
Text = "Parent 2",
Nodes = new ObservableCollection<Node>
{
new Node { Text = "Child 2.1"},
new Node { Text = "Child 2.2"},
}
},
}
};
}
}
Related
I have a DataGrid that have as Source an ObservableCollection of Site. I have some columns that are defined in a DataTemplate binding to properties of Site. The binding works perfectly when using builtin controls like TextBlock but I am not able to do it with a custom control. I have seen that this error appears in the console but I do not understand exactly what I have to do:
System.Windows.Data Error: 40 : BindingExpression path error: 'Port' property not found on 'object' ''TestControl' (Name='')'. BindingExpression:Path=Port; DataItem='TestControl' (Name=''); target element is 'TestControl' (Name=''); target property is 'CameraName' (type 'String')
Here I attach snippets of the code I use;
DataGrid:
<DataGrid Name="SitesList" CanUserReorderColumns="True"
ItemsSource="{Binding ViewModel.Sites}"
PreparingCellForEdit="DataGrid_PreparingCellForEdit"
>
<DataGrid.Resources>
<DataTemplate x:Key="CameraTemplate">
<Grid>
<hv:TestControl CameraName="{Binding Path=Port}"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="PortTemplate">
<TextBox x:Name="PortTextBox"
Text="{Binding Path=Port, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Header="Camera"
CellTemplate="{StaticResource CameraTemplate}"
MinWidth="100"/>
<DataGridTemplateColumn Header="Port"
CellTemplate="{StaticResource PortTemplate}"
MinWidth="70"/>
</DataGrid.Columns></DataGrid>
ObservableCollection:
private ObservableCollection<Site> m_sites = new ObservableCollection<Site>();
public ObservableCollection<Site> Sites
{
get
{
return m_sites;
}
set
{
m_sites = value;
}
}
Site.cs:
namespace Wizard.View
{
public class Site: INotifyPropertyChanged
{
#region properties
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
private string m_port = "0";
public string Port
{
get
{
return m_port;
}
set
{
m_port = value;
NotifyPropertyChanged("Port");
}
}
#endregion
}
}
TestControl.xaml.cs:
namespace Wizard.View.HelpersViews
{
public partial class TestControl: UserControl
{
public TestControl()
{
InitializeComponent();
}
public string CameraName
{
get { return (string)GetValue(CameraNameProperty); }
set {
Console.WriteLine(value);
SetValue(CameraNameProperty, value);
CameraNameTextBlock.Text = value;
}
}
public static readonly DependencyProperty CameraNameProperty =
DependencyProperty.Register("CameraName",
typeof(string),
typeof(TestControl),
new PropertyMetadata("0"));
}
}
TestControl.xaml:
<UserControl
x:Class="Wizard.View.HelpersViews.TestControl"
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"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
d:DesignHeight="100" d:DesignWidth="300">
<StackPanel Orientation="Horizontal" Height="Auto" HorizontalAlignment="Left">
<CheckBox Margin ="5,0" Name="AddCameraCheck"
VerticalAlignment="Center" Style="{StaticResource ConfiguratorCheckBox}" />
<TextBlock x:Name="CameraNameTextBlock" Width="175" Text="{Binding CameraName}" />
</StackPanel>
</UserControl>
The column Port is bound correctly but Camera is not.
The TextBlock in the TestControl should bind to the CameraName property of itself:
<TextBlock x:Name="CameraNameTextBlock" Width="175" Text="{Binding CameraName,
RelativeSource={RelativeSource AncestorType=UserControl}}" />
...but you should remove this for the control to inherit the DataContext from the DataGridRow:
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Also the CLR wrapper of a dependency property should only call GetValue and SetValue:
public string CameraName
{
get { return (string)GetValue(CameraNameProperty); }
set { SetValue(CameraNameProperty, value); }
}
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
Basically I have in my MainViewModel.cs:
ObservableCollection<TabItem> MyTabs { get; private set; }
However, I need to somehow be able to not only create the tabs, but have the tabs content be loaded and linked to their appropriate viewmodels while maintaining MVVM.
Basically, how can I get a usercontrol to be loaded as the content of a tabitem AND have that usercontrol wired up to an appropriate viewmodel. The part that makes this difficult is the ViewModel is not supposed to construct the actual view items, right? Or can it?
Basically, would this be MVVM appropriate:
UserControl address = new AddressControl();
NotificationObject vm = new AddressViewModel();
address.DataContext = vm;
MyTabs[0] = new TabItem()
{
Content = address;
}
I only ask because well, i'm constructing a View (AddressControl) from within a ViewModel, which to me sounds like a MVVM no-no.
This isn't MVVM. You should not be creating UI elements in your view model.
You should be binding the ItemsSource of the Tab to your ObservableCollection, and that should hold models with information about the tabs that should be created.
Here are the VM and the model which represents a tab page:
public sealed class ViewModel
{
public ObservableCollection<TabItem> Tabs {get;set;}
public ViewModel()
{
Tabs = new ObservableCollection<TabItem>();
Tabs.Add(new TabItem { Header = "One", Content = "One's content" });
Tabs.Add(new TabItem { Header = "Two", Content = "Two's content" });
}
}
public sealed class TabItem
{
public string Header { get; set; }
public string Content { get; set; }
}
And here is how the bindings look in the window:
<Window x:Class="WpfApplication12.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<ViewModel
xmlns="clr-namespace:WpfApplication12" />
</Window.DataContext>
<TabControl
ItemsSource="{Binding Tabs}">
<TabControl.ItemTemplate>
<!-- this is the header template-->
<DataTemplate>
<TextBlock
Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<!-- this is the body of the TabItem template-->
<DataTemplate>
<TextBlock
Text="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Window>
(Note, if you want different stuff in different tabs, use DataTemplates. Either each tab's view model should be its own class, or create a custom DataTemplateSelector to pick the correct template.)
A UserControl inside the data template:
<TabControl
ItemsSource="{Binding Tabs}">
<TabControl.ItemTemplate>
<!-- this is the header template-->
<DataTemplate>
<TextBlock
Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<!-- this is the body of the TabItem template-->
<DataTemplate>
<MyUserControl xmlns="clr-namespace:WpfApplication12" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
In Prism you usually make the tab control a region so that you don't have to take control over the bound tab page collection.
<TabControl
x:Name="MainRegionHost"
Regions:RegionManager.RegionName="MainRegion"
/>
Now the views can be added via registering itself into the region MainRegion:
RegionManager.RegisterViewWithRegion( "MainRegion",
( ) => Container.Resolve<IMyViewModel>( ).View );
And here you can see a speciality of Prism. The View is instanciated by the ViewModel. In my case I resolve the ViewModel throught a Inversion of Control container (e.g. Unity or MEF). The ViewModel gets the View injected via constructor injection and sets itself as the View's data context.
The alternative is to register the view's type into the region controller:
RegionManager.RegisterViewWithRegion( "MainRegion", typeof( MyView ) );
Using this approach allows you to create the views later during runtime, e.g. by a controller:
IRegion region = this._regionManager.Regions["MainRegion"];
object mainView = region.GetView( MainViewName );
if ( mainView == null )
{
var view = _container.ResolveSessionRelatedView<MainView>( );
region.Add( view, MainViewName );
}
Because you have registered the View's type, the view is placed into the correct region.
I have a Converter to decouple the UI and ViewModel,thats the point below:
<TabControl.ContentTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding Tab,Converter={StaticResource TabItemConverter}"/>
</DataTemplate>
</TabControl.ContentTemplate>
The Tab is a enum in my TabItemViewModel and the TabItemConverter convert it to the real UI.
In the TabItemConverter,just get the value and Return a usercontrol you need.
My solution uses ViewModels directly, so I think it might be useful to someone:
First, I bind the Views to the ViewModels in the App.xaml file:
<Application.Resources>
<DataTemplate DataType="{x:Type local:ViewModel1}">
<local:View1/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ViewModel2}">
<local:View2/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ViewModel3}">
<local:View3/>
</DataTemplate>
</Application.Resources>
The MainViewModel looks like this:
public class MainViewModel : ObservableObject
{
private ObservableCollection<ViewModelBase> _viewModels = new ObservableCollection<ViewModelBase>();
public ObservableCollection<ViewModelBase> ViewModels
{
get { return _viewModels; }
set
{
_viewModels = value;
OnPropertyChanged();
}
}
private ViewModelBase _currentViewModel;
public ViewModelBase CurrentViewModel
{
get { return _currentViewModel; }
set
{
_currentViewModel = value;
OnPropertyChanged();
}
}
private ICommand _closeTabCommand;
public ICommand CloseTabCommand => _closeTabCommand ?? (_closeTabCommand = new RelayCommand(p => closeTab()));
private void closeTab()
{
ViewModels.Remove(CurrentViewModel);
CurrentViewModel = ViewModels.LastOrDefault();
}
private ICommand _openTabCommand;
public ICommand OpenTabCommand => _openTabCommand ?? (_openTabCommand = new RelayCommand(p => openTab(p)));
private void openTab(object selectedItem)
{
Type viewModelType;
switch (selectedItem)
{
case "1":
{
viewModelType = typeof(ViewModel1);
break;
}
case "2":
{
viewModelType = typeof(ViewModel2);
break;
}
default:
throw new Exception("Item " + selectedItem + " not set.");
}
displayVM(viewModelType);
}
private void displayVM(Type viewModelType)
{
if (!_viewModels.Where(vm => vm.GetType() == viewModelType).Any())
{
ViewModels.Add((ViewModelBase)Activator.CreateInstance(viewModelType));
}
CurrentViewModel = ViewModels.Single(vm => vm.GetType() == viewModelType);
}
}
}
MainWindow.XAML:
<Window.DataContext>
<local:MainWindowViewModel x:Name="vm"/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Menu Grid.Row="0">
<MenuItem Header="1" Command="{Binding OpenTabCommand}" CommandParameter="1"/>
<MenuItem Header="2" Command="{Binding OpenTabCommand}" CommandParameter="2"/>
<MenuItem Header="3" Command="{Binding OpenTabCommand}" CommandParameter="3"/>
</Menu>
<TabControl Grid.Row="1" ItemsSource="{Binding ViewModels}" SelectedItem="{Binding CurrentViewModel}">
<TabControl.ItemTemplate>
<DataTemplate DataType="{x:Type MVVMLib:ViewModelBase}">
<TextBlock Text="{Binding Title}">
<Hyperlink Command="{Binding RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}, Path=DataContext.CloseWindowCommand}">X</Hyperlink>
</TextBlock>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
</Grid>
I translated some parts to make it easier to understand, there might be some typos.
I have a ListBox where I'm using a UserControl as DataTemplate. My UserControl has a ViewModel. I have a DependencyProperty in my UserControl so that I can bind item from my ListBox to my UserControl.
It does not work unless I do not set any DataContext to my UserControl.
How can I use DP and custom DataContext in my UC ?
My ListBox:
<ListBox ItemsSource="{Binding Path=ListItems, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<local:MyCustomUC MyObject="{Binding Path=.}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
My UserControl XAML:
<UserControl x:Class="UserControlDataTemplate.MyCustomUC"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="Auto" Width="Auto">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=FromViewModel}" />
<Button Content="{Binding ElementName=MyObject, Path=FromParent}" />
</StackPanel>
</UserControl>
My UserControl CS:
public MyClass MyObject
{
get { return (MyClass)GetValue(MyObjectProperty); }
set
{
SetValue(MyObjectProperty, value);
}
}
// Using a DependencyProperty as the backing store. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MyObjectProperty =
DependencyProperty.Register("MyObject", typeof(MyClass), typeof(MyCustomUC), new PropertyMetadata(null));
public MyCustomUC()
{
InitializeComponent();
this.DataContext = new MyCustomUCViewModel();
}
My ViewModel:
public class MyCustomUCViewModel : DependencyObject, INotifyPropertyChanged
{
public String FromViewModel { get; set; }
public MyCustomUCViewModel()
{
this.FromViewModel = Guid.NewGuid().ToString();
}
...
}
Item class in ItemSource from ListBox:
public class MyClass : INotifyPropertyChanged
{
public String FromParent { get; set; }
...
}
What did I do wrong ?
Here you are setting the DataContext in the MyCustomUC()
instead you can set DataContext like this
<vm:YourViewModel x:Name="VModel" IfPropertToSet="{BindingFromExistingDC}"/>
<ListBox ItemsSource="{Binding Path=ListItems, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<local:MyCustomUC MyObject="{Binding Path=.}" DataContext="{Binding ElementName=VModel}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
you need to include the namespace
xmlns:vm="clr-namespace:YourViewModelPath"
I'm getting started with WPF and trying to get my head around connecting data to the UI. I've managed to connect to a class without any issues, but what I really want to do is connect to a property of the main window.
Here's the XAML:
<Window x:Class="test3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:test3"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<CollectionViewSource
Source="{Binding Source={x:Static Application.Current}, Path=Platforms}"
x:Key="platforms"/>
<DataTemplate DataType="{x:Type custom:Platform}">
<StackPanel>
<CheckBox IsChecked="{Binding Path=Selected}"/>
<TextBlock Text="{Binding Path=Name}"/>
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox ItemsSource="{Binding Source={StaticResource platforms}}"/>
</Grid>
Here's the code for the main window:
public partial class MainWindow : Window
{
ObservableCollection<Platform> m_platforms;
public MainWindow()
{
m_platforms = new ObservableCollection<Platform>();
m_platforms.Add(new Platform("PC"));
InitializeComponent();
}
public ObservableCollection<Platform> Platforms
{
get { return m_platforms; }
set { m_platforms = value; }
}
}
Here's the Platform class:
public class Platform
{
private string m_name;
private bool m_selected;
public Platform(string name)
{
m_name = name;
m_selected = false;
}
public string Name
{
get { return m_name; }
set { m_name = value; }
}
public bool Selected
{
get { return m_selected; }
set { m_selected = value; }
}
}
This all compiles and runs fine but the list box displays with nothing in it. If I put a breakpoint on the get method of Platforms, it doesn't get called. I don't understand as Platforms is what the XAML should be connecting to!
Your code looks ok apart from the fact that the Binding on Source on CollectionViewSource is not correct. You probably meant this:
<CollectionViewSource
Source="{Binding Source={x:Static Application.Current}, Path=MainWindow.Platforms}"
x:Key="platforms"/>
Without this change the Binding actually looked for property Platforms on Application instance.
I would suggest you add the platforms not to the MainWindow but rather set it as the MainWindow's DataContext (wrapped inside a ViewModel).
That way you can very easily bind against it (the binding code would look like ItemsSource={Binding Path=Platforms}).
This is part of WPFs design, that every form should have a explicit DataContext it binds to.
A somewhat more appropriate solution is to give your window a name. A nice convention is _this.
<Window x:Name="_this" x:Class="test3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:test3"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<CollectionViewSource
Source="{Binding ElementName=_this, Path=Platforms}"
x:Key="platforms"/>
<DataTemplate DataType="{x:Type custom:Platform}">
<StackPanel>
<CheckBox IsChecked="{Binding Path=Selected}"/>
<TextBlock Text="{Binding Path=Name}"/>
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox ItemsSource="{Binding Source={StaticResource platforms}}"/>
</Grid>
Here's my updated code, the XAML:
<Window x:Name="_this"
x:Class="test3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:test3"
Title="MainWindow" Height="190" Width="177">
<Window.Resources>
<CollectionViewSource
Source="{Binding ElementName=_this, Path=Platforms}"
x:Key="platforms"/>
<DataTemplate x:Key="platformTemplate" DataType="{x:Type custom:Platform}">
<StackPanel Orientation="Horizontal">
<CheckBox Margin="1" IsChecked="{Binding Path=Selected}"/>
<TextBlock Margin="1" Text="{Binding Path=Name}"/>
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="23" />
<RowDefinition Height="23" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
ItemsSource="{Binding Source={StaticResource platforms}}"
ItemTemplate="{StaticResource platformTemplate}"/>
<Button Click="OnBuild" Grid.Row="1">Build...</Button>
<Button Click="OnTogglePC" Grid.Row="2">Toggle PC</Button>
</Grid>
</Window>
The code behind the XAML:
public partial class MainWindow : Window
{
ObservableCollection<Platform> m_platforms;
public MainWindow()
{
m_platforms = new ObservableCollection<Platform>();
m_platforms.Add(new Platform("PC"));
m_platforms.Add(new Platform("PS3"));
m_platforms.Add(new Platform("Xbox 360"));
InitializeComponent();
}
public ObservableCollection<Platform> Platforms
{
get { return m_platforms; }
set { m_platforms = value; }
}
private void OnBuild(object sender, RoutedEventArgs e)
{
string text = "";
foreach (Platform platform in m_platforms)
{
if (platform.Selected)
{
text += platform.Name + " ";
}
}
if (text == "")
{
text = "none";
}
MessageBox.Show(text, "WPF TEST");
}
private void OnTogglePC(object sender, RoutedEventArgs e)
{
m_platforms[0].Selected = !m_platforms[0].Selected;
}
}
...and finally the Platform code, enhanced to finish off the two way interaction:
public class Platform : INotifyPropertyChanged
{
private string m_name;
private bool m_selected;
public Platform(string name)
{
m_name = name;
m_selected = false;
}
public string Name
{
get { return m_name; }
set
{
m_name = value;
OnPropertyChanged("Name");
}
}
public bool Selected
{
get { return m_selected; }
set
{
m_selected = value;
OnPropertyChanged("Selected");
}
}
private void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Using DataContext, it gets even easier!
<Window x:Class="test5.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:test5"
Title="MainWindow" Height="190" Width="177">
<Window.Resources>
<CollectionViewSource
Source="{Binding Path=.}"
x:Key="platforms"/>
<DataTemplate x:Key="platformTemplate" DataType="{x:Type custom:Platform}">
<StackPanel Orientation="Horizontal">
<CheckBox Margin="1" IsChecked="{Binding Path=Selected}"/>
<TextBlock Margin="1" Text="{Binding Path=Name}"/>
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="23" />
<RowDefinition Height="23" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
ItemsSource="{Binding Source={StaticResource platforms}}"
ItemTemplate="{StaticResource platformTemplate}"/>
<Button Click="OnBuild" Grid.Row="1">Build...</Button>
<Button Click="OnTogglePC" Grid.Row="2">Toggle PC</Button>
</Grid>
</Window>
Here's the code behind this:
private ObservableCollection<Platform> m_platforms;
public MainWindow()
{
InitializeComponent();
m_platforms = new ObservableCollection<Platform>();
m_platforms.Add(new Platform("PC"));
m_platforms.Add(new Platform("PS3"));
m_platforms.Add(new Platform("Xbox 360"));
DataContext = m_platforms;
}
public void OnBuild(object sender, RoutedEventArgs e)
{
string text = "";
foreach (Platform platform in m_platforms)
{
if (platform.Selected)
{
text += platform.Name + " ";
}
}
if (text == "")
{
text = "none";
}
MessageBox.Show(text, "WPF TEST");
}
public void OnTogglePC(object sender, RoutedEventArgs e)
{
m_platforms[0].Selected = !m_platforms[0].Selected;
}
Note that I've dropped the need to declare Platforms as a property of the main window, instead I assign it to the DataContext, and the XAML source becomes, simply, "."