I'm working with WPF (MVVM pattern in particular) and I'm trying to create a simple application that shows a list of tasks. I created a custom control called TaskListControl that shows a list of other custom controls named TaskListItemControl and each of them has its own ViewModel.
Here is the TaskListItemControl template, where you can see the InputBindings and the Triggers that affects the control appearence when the IsSelected is set to true:
<UserControl x:Class="CSB.Tasks.TaskListItemControl"
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"
xmlns:local="clr-namespace:CSB.Tasks"
mc:Ignorable="d"
d:DesignHeight="70"
d:DesignWidth="400">
<!-- Custom control that represents a Task. -->
<UserControl.Resources>
<!-- The control style. -->
<Style x:Key="ContentStyle" TargetType="{x:Type ContentControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ContentControl}">
<Border x:Name="ContainerBorder" BorderBrush="{StaticResource LightVoidnessBrush}"
Background="{StaticResource VoidnessBrush}"
BorderThickness="1"
Margin="2">
<Border.InputBindings>
<MouseBinding MouseAction="LeftClick" Command="{Binding SelctTaskCommand}"/>
</Border.InputBindings>
<!-- The grid that contains the control. -->
<Grid Name="ContainerGrid" Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Border representing the priority state of the Task:
The color is defined by a ValueConverter according to the PriorityLevel of the Task object. -->
<Border Grid.Column="0"
Width="10"
Background="{Binding Priority, Converter={local:PriorityLevelToRGBConverter}}">
</Border>
<!-- Border containing the Task's informations. -->
<Border Grid.Column="1" Padding="5">
<StackPanel>
<!-- The title of the Task. -->
<TextBlock Text="{Binding Title}" FontSize="{StaticResource TaskListItemTitleFontSize}" Foreground="{StaticResource DirtyWhiteBrush}"/>
<!-- The customer the Taks refers to. -->
<TextBlock Text="{Binding Customer}" Style="{StaticResource TaskListItemControlCustomerTextBlockStyle}"/>
<!-- The description of the Task. -->
<TextBlock Text="{Binding Description}"
TextTrimming="WordEllipsis"
Foreground="{StaticResource DirtyWhiteBrush}"/>
</StackPanel>
</Border>
<!-- Border that contains the controls for the Task management. -->
<Border Grid.Column="2"
Padding="5">
<!-- Selection checkbox of the Task. -->
<CheckBox Grid.Column="2" VerticalAlignment="Center"/>
</Border>
</Grid>
</Border>
<!-- Template triggers. -->
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter Property="Background" TargetName="ContainerBorder" Value="{StaticResource OverlayVoidnessBrush}"/>
<Setter Property="BorderBrush" TargetName="ContainerBorder" Value="{StaticResource PeterriverBrush}"/>
</DataTrigger>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Duration="0:0:0:0" To="{StaticResource OverlayVoidness}" Storyboard.TargetName="ContainerGrid" Storyboard.TargetProperty="Background.Color"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Duration="0:0:0:0" To="Transparent" Storyboard.TargetName="ContainerGrid" Storyboard.TargetProperty="Background.Color"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<!-- Content of the control: assignment of the DataContext for design-time testing. -->
<ContentControl d:DataContext="{x:Static local:TaskListItemDesignModel.Instance}"
Style="{StaticResource ContentStyle}"/>
And here is the TaskListItemViewModel where the Action should be executed (all the PropertyChanged boilerplate code is handled inside the BaseViewModel class):
/// <summary>
/// The ViewModel for the <see cref="TaskListItemControl"/>.
/// </summary>
public class TaskListItemViewModel : BaseViewModel
{
#region Public Properties
/// <summary>
/// Priority level of the task.
/// </summary>
public PriorityLevel Priority { get; set; }
/// <summary>
/// The name of the task.
/// </summary>
public string Title { get; set; }
/// <summary>
/// The customer the task refers to.
/// </summary>
public string Customer { get; set; }
/// <summary>
/// The description of the task.
/// </summary>
public string Description { get; set; }
/// <summary>
/// True if the Task is the selected one in the task list.
/// </summary>
public bool IsSelected { get; set; }
#endregion
#region Commands
/// <summary>
/// The command fired whenever a task is selected.
/// </summary>
public ICommand SelectTaskCommand { get; set; }
#endregion
#region Constructor
/// <summary>
/// The <see cref="TaskListItemViewModel"/> default constructor.
/// </summary>
public TaskListItemViewModel()
{
// Initialize commands.
// When the task is selected, IsSelected becomes true.
// The command will do other stuff in the future.
SelectTaskCommand = new RelayCommand(() => IsSelected = true);
}
#endregion
}
The data is provided through a design model bound to the TaskListControl control where the properties of each item of the list are hardcoded (this design model will be replaced with a database since this class just provides dummy data):
/// <summary>
/// The <see cref="TaskListControl"/> design model that provides dummy data for the XAML testing.
/// </summary>
public class TaskListDesignModel : TaskListViewModel
{
#region Public Properties
/// <summary>
/// A single instance of the <see cref="TaskListDesignModel"/> class.
/// </summary>
public static TaskListDesignModel Instance => new TaskListDesignModel();
#endregion
#region Constructor
/// <summary>
/// The <see cref="TaskListDesignModel"/> default constructor that provides dummy data.
/// </summary>
public TaskListDesignModel()
{
Items = new ObservableCollection<TaskListItemViewModel>
{
new TaskListItemViewModel
{
Title = "Activity #1",
Customer = "Internal",
Description = "This is activity #1.",
Priority = PriorityLevel.High,
IsSelected = false
},
new TaskListItemViewModel
{
Title = "Activity #2",
Customer = "Internal",
Description = "This is activity #2.",
Priority = PriorityLevel.High,
IsSelected = false
},
new TaskListItemViewModel
{
Title = "Activity #3",
Customer = "Internal",
Description = "This is activity #3.",
Priority = PriorityLevel.High,
IsSelected = false
},
new TaskListItemViewModel
{
Title = "Activity #4",
Customer = "Internal",
Description = "This is activity #4.",
Priority = PriorityLevel.Medium,
IsSelected = false
},
new TaskListItemViewModel
{
Title = "Activity #5",
Customer = "Internal",
Description = "This is activity #5.",
Priority = PriorityLevel.Medium,
IsSelected = false
},
new TaskListItemViewModel
{
Title = "Activity #6",
Customer = "Internal",
Description = "This is activity #6.",
Priority = PriorityLevel.Low,
IsSelected = false
}
};
}
#endregion
}
Here is the result:
What I want to do when the list item is selected is to update his IsSelected property in the ViewModel and change its appearence through Triggers, but nothing happens when I click on an item.
Here is the link to the GitHub repository of the entire project if needed.
What am I missing? Thank you in advance for the help.
You should fix this two problems to solve the selection issue:
1) I spotted a typo inside your TaskListItemControl:
Line 25 should be "SelectTaskCommand" on the Command binding.
This will finally invoke the command.
2) Then in your TaskListItemViewModel you have to make your properties raise the PropertyChanged event. I highly recommend this for all your ViewModel properties. But to solve your problem this must apply at least to the IsSelected property:
private bool isSelected;
public bool IsSelected
{
get => this.isSelected;
set
{
if (value.Equals(this.isSelected)
{
return;
}
this.isSelected = value;
OnPropertyChanged();
}
}
This will make any changes to IsSelected propagate and thus the DataTrigger can work as expected.
And just another recommendation is to modify the PropertyChanged invocator signature in BaseViewModel to:
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
This way you avoid to always pass the property name as an argument.
If you want the CheckBox to reflect the selection state and should be used to undo the selection, just add a TwoWay binding to its IsChecked property:
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" />
Related
I am trying to create an application using WPF. I am trying to fully build it using the MVVM model. However, I am puzzled on how to correctly display the error message. I thought it would be trivial step but seems to be the most complex.
I created the following view using xaml
<StackPanel Style="{StaticResource Col}">
<DockPanel>
<Grid DockPanel.Dock="Top">
<Grid.ColumnDefinitions >
<ColumnDefinition Width="*" ></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Style="{StaticResource Col}">
<Label Content="Name" Style="{StaticResource FormLabel}" />
<Border Style="{StaticResource FormInputBorder}">
<TextBox x:Name="Name" Style="{StaticResource FormControl}" Text="{Binding Name, ValidatesOnDataErrors=True, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" />
</Border>
</StackPanel>
<StackPanel Grid.Column="1" Style="{StaticResource Col}">
<Label Content="Phone Number" Style="{StaticResource FormLabel}" />
<Border Style="{StaticResource FormInputBorder}">
<TextBox x:Name="Phone" Style="{StaticResource FormControl}" Text="{Binding Phone, ValidatesOnDataErrors=True, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" />
</Border>
</StackPanel>
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Style="{StaticResource PrimaryButton}" Command="{Binding Create}">Create</Button>
<Button>Reset</Button>
</StackPanel>
</DockPanel>
</StackPanel>
Then I created the following ViewModel
public class VendorViewModel : ViewModel
{
protected readonly IUnitOfWork UnitOfWork;
private string _Name { get; set; }
private string _Phone { get; set; }
public VendorViewModel()
: this(new UnitOfWork())
{
}
public VendorViewModel(IUnitOfWork unitOfWork)
{
UnitOfWork = unitOfWork;
}
[Required(ErrorMessage = "The name is required")]
[MinLength(5, ErrorMessage = "Name must be more than or equal to 5 letters")]
[MaxLength(50, ErrorMessage = "Name must be less than or equal to 50 letters")]
public string Name
{
get { return _Name; }
set
{
_Name = value;
NotifyPropertyChanged();
}
}
public string Phone
{
get { return _Phone; }
set
{
_Phone = value;
NotifyPropertyChanged();
}
}
/// <summary>
/// Gets the collection of customer loaded from the data store.
/// </summary>
public ICollection<Vendor> Vendors { get; private set; }
protected void AddVendor()
{
var vendor = new Vendor(Name, Phone);
UnitOfWork.Vendors.Add(vendor);
}
public ICommand Create
{
get
{
return new ActionCommand(p => AddVendor(),
p => IsValidRequest());
}
}
public bool IsValidRequest()
{
// There got to be a better way to check if everything passed or now...
return IsValid("Name") && IsValid("Phone");
}
}
Here is how my ViewModel base class look like
public abstract class ViewModel : ObservableObject, IDataErrorInfo
{
/// <summary>
/// Gets the validation error for a property whose name matches the specified <see cref="columnName"/>.
/// </summary>
/// <param name="columnName">The name of the property to validate.</param>
/// <returns>Returns a validation error if there is one, otherwise returns null.</returns>
public string this[string columnName]
{
get { return OnValidate(columnName); }
}
/// <summary>
/// Validates a property whose name matches the specified <see cref="propertyName"/>.
/// </summary>
/// <param name="propertyName">The name of the property to validate.</param>
/// <returns>Returns a validation error, if any, otherwise returns null.</returns>
protected virtual string OnValidate(string propertyName)
{
var context = new ValidationContext(this)
{
MemberName = propertyName
};
var results = new Collection<ValidationResult>();
bool isValid = Validator.TryValidateObject(this, context, results, true);
if (!isValid)
{
ValidationResult result = results.SingleOrDefault(p => p.MemberNames.Any(memberName => memberName == propertyName));
if (result != null)
return result.ErrorMessage;
}
return null;
}
protected virtual bool IsValid(string propertyName)
{
return OnValidate(propertyName) == null;
}
/// <summary>
/// Not supported.
/// </summary>
[Obsolete]
public string Error
{
get
{
throw new NotSupportedException();
}
}
}
Here is my ObservableObject class
public class ObservableObject : INotifyPropertyChanged
{
/// <summary>
/// Raised when the value of a property has changed.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises <see cref="PropertyChanged"/> for the property whose name matches <see cref="propertyName"/>.
/// </summary>
/// <param name="propertyName">Optional. The name of the property whose value has changed.</param>
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
My goal is to show a red border around the incorrect field then display the error message right underneath it to tell the use what went wrong.
How do I show the error correctly? Also, how to I not show any error when the view is first loaded?
Base on this blog I need to edit the Validation.ErrorTemplate
So I tried adding the following code to the App.xaml file
<!-- Style the error validation by showing the text message under the field -->
<Style TargetType="TextBox">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<Border BorderThickness="1" BorderBrush="DarkRed">
<StackPanel>
<AdornedElementPlaceholder x:Name="errorControl" />
</StackPanel>
</Border>
<TextBlock Text="{Binding AdornedElement.ToolTip, ElementName=errorControl}" Foreground="Red" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="BorderBrush" Value="Red" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
But that isn't showing the error message, also when the view is first loaded I get an error. Finally, even when the form become valid, the action button stay disabled.
UPDATED
After moving the Property="Validation.ErrorTemplate" into the FormControl group it worked. However, the error message seems to be going over the buttons instead of pushing the buttons down. Also, the text does not seems to be wrapping vertically allowing the border to strach over the other control as you can see in the following screen shows.
I'll try to answer all your questions:
How do I show the error correctly?
The ErrorTemplate is not applied because the FormControl style on your TextBox has precedence over the style containing the Validation.ErrorTemplate. Moving the Validation.ErrorTemplate code into the FormControl style will fix this issue.
Also, how to I not show any error when the view is first loaded?
What is the use of a Required validation if it's not applied immediately? The MinLength and MaxLength validations will only be executed when you start typing in the field.
However, the error message seems to be going over the buttons instead of pushing the buttons down.
As Will pointed out, this is because the error messages are shown on the AdornerLayer, which cannot interfere with the layer your controls are on. You have the following options:
Use the AdornerLayer but leave some room between controls
Use a ToolTip to show the error messages
Use an extra TextBlock in the template of the TextBox that shows the error messages.
These options are described here
I'm a complete noob to MVVM. Until now, I managed to bind properties from my loaded plugins (which are in a collection) to my textboxes as well as the available plugins to some comboboxes.
Now, I'm struggling with displaying their UI in the host application.
Everywhere, I read something about DataTemplate or MEF, but I really have no idea how to do that. Looks good on paper, but I have no idea how to implement it.
I load my plugins using Reflection.
Is it possible like that?
Everything I need to load is stored in Connections:
Connection.cs
public class Connection
{
public string ConnectionName { get; set; }
public Plugin<IPlugin> ERP { get; set; }
public Plugin<IPlugin> Shop { get; set; }
public Connection(string connName)
{
ConnectionName = connName;
}
}
Plugin.cs
public class Plugin<T>
{
public Assembly PluginAssembly { get; set; }
public Type[] TypeList { get; set; }
public T PluginInstance { get; set; }
public Type PluginType { get; set; }
public Plugin()
{
}
}
IPlugin.cs
public interface IPlugin
{
/// <summary>
/// Contains information about the plugin, like version, author and type
/// </summary>
PluginInfo Info { get; }
/// <summary>
/// List of field lists for different mappings
/// </summary>
List<Fieldlist> Fieldlist { get; set; }
UserControl UI { get; }
Config Config { get; set; }
/// <summary>
/// Gets called at the start of the plugin
/// </summary>
void Load();
/// <summary>
/// Import function for transferring data into the plugin
/// </summary>
/// <param name="json">The data to be transferred</param>
/// <param name="type">The type of data e.g articles, orders, etc.</param>
void Import(string json, string type);
/// <summary>
/// Export function for transferring data out of the plugin
/// </summary>
/// <param name="type">The type of data e.g articles, orders, etc.</param>
/// <returns>The JSON to be transferred</returns>
string Export(string type);
}
And the "affected" part my MainWindow.xaml:
<TabItem Header="ERP">
<ContentControl Name="ccERP" Background="#FFE5E5E5" Content="{Binding ElementName=lbVerbindungen, Path=SelectedItem.ERP.PluginInstance.UI}"/>
</TabItem>
<TabItem Header="Shop">
<ContentControl Name="ccShop" Background="#FFE5E5E5" Content="{Binding ElementName=lbVerbindungen, Path=SelectedItem.Shop.PluginInstance.UI}"/>
</TabItem>
EDIT: My application loads the mentioned plugins at runtime using assemblies as plugins, which need to have an UI of some sort. Setting the content of the ContentControl in the code works fine using ccERP.Content = ((Connection)lbVerbindungen.SelectedItem).ERP.PluginInstance.UI;, but it doesn't using the binding I used above.
EDIT2: My listbox is done like this:
<ListBox x:Name="lbVerbindungen" HorizontalAlignment="Left" Margin="10,10,0,43" Width="208" IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding Connections}" DisplayMemberPath="ConnectionName">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<EventSetter Event="Loaded" Handler="ListBoxItem_Loaded"/>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True" >
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="Black" />
</Trigger>
</Style.Triggers>
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Black"/>
<!-- Background of selected item when focussed -->
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="LightCyan"/>
<!-- Background of selected item when not focussed -->
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="LightGray" />
</Style.Resources>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
The Plugin interface/class shouldn't have a property that returns a UserControl. Instead you should define a DataTemplate in the view that defines the appearance of a plugin, e.g.:
<ContentControl Name="ccERP" Background="#FFE5E5E5"
Content="{Binding ElementName=lbVerbindungen, Path=SelectedItem.ERP.PluginInstance}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type local:IPlugin}">
<local:PluginUserControl />
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
An implicit DataTemplate without an x:Key will be automatically applied to all objects of the type specified by the DataType property.
You can read more about data templating on MSDN: https://msdn.microsoft.com/en-us/library/ms742521(v=vs.110).aspx
Edit:
Add a SelectedConnection property to your main window view model class:
private Connection _connection;
public Connection SelectedConnection
{
get
{
return _connection;
}
set
{
_connection = value;
OnProprtyChanged();
}
}
...and bind the SelectedItem property of the ListBox to this one:
<ListBox x:Name="lbVerbindungen" .... ItemsSource="{Binding Connections}" SelectedItem="{Binding SelectedConnection}" DisplayMemberPath="ConnectionName">
...
Then you should be able to bind the Content property of the ContentControl to the UI property like this:
<TabControl>
<TabItem Header="ERP">
<ContentControl Name="ccERP" Background="#FFE5E5E5"
Content="{Binding DataContext.SelectedConnection.ERP.PluginInstance.UI, RelativeSource={RelativeSource AncestorType=Window}}"/>
</TabItem>
</TabControl>
Hi i' m trying to create a custom TabItem with delete Button,i want to bind my viewmodel command to my custom Dependency Property 'DeleteCommandProperty' . Can someone tell me what i'm doing wrong?
my Custom TabControl :
/// <summary>
/// TabControl withCustom TabItem
/// </summary>
public class MyTabControl:TabControl
{
/// <summary>
/// TabItem override
/// </summary>
/// <returns></returns>
protected override DependencyObject GetContainerForItemOverride()
{
return new MyTabItem();
}
}
my Custom TabItem class :
/// <summary>
/// Custom TabItem
/// </summary>
public class MyTabItem:TabItem
{
/// <summary>
/// Delete Command
/// </summary>
public static DependencyProperty DeleteCommandProperty = DependencyProperty.Register(
"DeleteCommand",typeof(ICommand),typeof(MyTabItem));
/// <summary>
/// Delete
/// </summary>
public ICommand DeleteCommand
{
get { return (ICommand)GetValue(DeleteCommandProperty); }
set { SetValue(DeleteCommandProperty, value); }
}
}
when i bind the DeleteCommand directly like this my Command in my ViewModel is executed
<customControls:MyTabControl>
<customControls:MyTabItem Header="Test" DeleteCommand="{Binding DeleteStudiengangCommand}" Template="{DynamicResource MyTabItemControlTemplate}"/>
</customControls:MyTabControl>
bu when try to bind the deleteCommand via style like this but it doesn't work :
<Style TargetType="customControls:MyTabItem">
<Setter Property="Template" Value="{DynamicResource MyTabItemControlTemplate}"/>
<Setter Property="DeleteCommand" Value="{Binding MyDeleteCommand}"/>
</Style>
<customControls:MyTabControl ItemsSource="{Binding MyList}" SelectedItem="{Binding SelectedItem}" SelectedIndex="0">
<customControls:MyTabControl.ContentTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding Value}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</customControls:MyTabControl.ContentTemplate>
</customControls:MyTabControl>
TL;DR
I suspect your DataContext contains your current item from MyList (whatever it is), so the style setter cannot access the MyDeleteCommand property as you planned. Look at the visual studio debugger log, whether a binding exception is logged there.
Example that evolved from a working code until it happened to reveal a possible problem explanation.
It seems you have some difficulties in reducing your example, so the only thing I can offer you based on the information you provide is a small working example with the Style and TemplateBinding approach which you can use as a base to identify your real problem. Edit: The explanation at the end might actually be the answer to your question.
Note you may have to change the namespaces to match with your project setup.
MainWindow.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"
Loaded="Window_Loaded">
<Window.Resources>
<!-- Header template -->
<ControlTemplate x:Key="MyTabItemControlTemplate" TargetType="{x:Type local:MyTabItem}">
<!-- Some text and the command button with template binding -->
<StackPanel Orientation="Horizontal" Background="Aquamarine" Margin="3">
<ContentPresenter Content="{TemplateBinding Header}" VerticalAlignment="Center" Margin="2"/>
<Button Content="Delete" Command="{TemplateBinding DeleteCommand}" Margin="2"/>
</StackPanel>
</ControlTemplate>
<!-- Setting the control template and assigning the command implementation -->
<Style TargetType="{x:Type local:MyTabItem}">
<Setter Property="Template" Value="{DynamicResource MyTabItemControlTemplate}"/>
<Setter Property="DeleteCommand" Value="{Binding MyDeleteCommand}"/>
<Setter Property="Header" Value="Default Header Text"/>
</Style>
</Window.Resources>
<Grid>
<local:MyTabControl ItemsSource="{Binding MyTabItemList}"/>
</Grid>
</Window>
MainWindow.xaml.cs
/// <summary>
/// TabControl withCustom TabItem
/// </summary>
public class MyTabControl : TabControl
{
/// <summary>
/// TabItem override
/// </summary>
protected override DependencyObject GetContainerForItemOverride()
{
return new MyTabItem();
}
}
public class MyTabItem : TabItem
{
/// <summary>
/// Delete Command
/// </summary>
public static DependencyProperty DeleteCommandProperty = DependencyProperty.Register(
"DeleteCommand", typeof(ICommand), typeof(MyTabItem));
/// <summary>
/// Delete
/// </summary>
public ICommand DeleteCommand
{
get { return (ICommand)GetValue(DeleteCommandProperty); }
set { SetValue(DeleteCommandProperty, value); }
}
}
public class MyCommand : ICommand
{
public void Execute(object parameter)
{
MessageBox.Show("Hello WPF", "Message");
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged { add { } remove { } }
}
public class MyContext
{
public ICommand MyDeleteCommand { get; set; }
public List<object> MyTabItemList { get; set; }
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
var list = new List<object>();
list.Add(new TextBlock() { Text = "Test 1" });
list.Add(new MyTabItem() { Content = "Test Content 2", Header = "Test Header 2" });
list.Add(new TabItem() { Content = "Test Content 3", Header = "Test Header 3" });
this.DataContext = new MyContext()
{
MyTabItemList = list,
MyDeleteCommand = new MyCommand()
};
}
}
Summary of the example:
You see three different tabs, each with its unique build and appearance:
The tab item is created from the GetContainerForItemOverride method, the style setter for the Header property specifies the text that appears within the header. The MyDeleteCommand binding does not work!
The tab item is provided as MyTabItem with header and content value. The style setter for the Header property does not apply, because the property is explicitely set. The style setter for the DeleteCommand property binds to the MyDeleteCommand property from MyContext.
The tab item is provided as TabItem and because there is no override of MyTabControl.IsItemItsOwnContainerOverride, this is accepted as a control item of MyTabControl. The whole MyTabItem template does not apply.
The debugging output inside visual studio gives a hint about the underlying problem for the first tab item:
System.Windows.Data Error: 40 : BindingExpression path error: 'MyDeleteCommand' property not found on 'object' ''TextBlock' (Name='')'. BindingExpression:Path=MyDeleteCommand; DataItem='TextBlock' (Name=''); target element is 'MyTabItem' (Name=''); target property is 'DeleteCommand' (type 'ICommand')
The reason is, that the current tab item content becomes the new local DataContext in this scenario unlike the second tab item, where the item itself is accepted as the container.
A solution could be to ensure usage of the propper context on the command binding:
Supposed you give some suitable parent element a name x:Name="_this", then you can access the parent DataContext.
<Setter Property="DeleteCommand" Value="{Binding DataContext.MyDeleteCommand,ElementName=_this}"/>
I am working with a team on LoB application. We would like to have a dynamic Menu control, which creates the menu based on the logged in user profile. In previous development scenarios (namely ASP.NET) we use to iterate through data which describes collection and generate MenuItem dynamically. In MVVM how would I do this? Can I separate XAML view from ViewModel which describes menu elements?
Solution:
With inputs from commentators I were able to bind Menu dynamically with the data from ViewModel. This article was of great help too.
XAML:
<HierarchicalDataTemplate DataType="{x:Type self:Menu}" ItemsSource="{Binding Path=Children, UpdateSourceTrigger=PropertyChanged}">
<ContentPresenter Content="{Binding Path=MenuText}" RecognizesAccessKey="True"/>
</HierarchicalDataTemplate>
[...]
<Menu Height="21" Margin="0" Name="mainMenu" VerticalAlignment="Top" HorizontalAlignment="Stretch"
ItemsSource="{Binding Path=MenuItems, UpdateSourceTrigger=PropertyChanged}" ItemContainerStyle="{StaticResource TopMenuItems}">
<Menu.Background>
<ImageBrush ImageSource="/Wpf.Modules;component/Images/MenuBg.jpg" />
</Menu.Background>
</Menu>
Menu data class:
public class Menu : ViewModelBase
{
public Menu()
{
IsEnabled = true;
Children = new List<Menu>();
}
#region [ Menu Properties ]
private bool _isEnabled;
private string _menuText;
private ICommand _command;
private IList<Menu> _children;
public string MenuText
{
get { return _menuText; }
set
{
_menuText = value;
base.OnPropertyChanged("MenuText");
}
}
public bool IsEnabled
{
get { return _isEnabled; }
set
{
_isEnabled = value;
base.OnPropertyChanged("IsEnabled");
}
}
public ICommand Command
{
get { return _command; }
set
{
_command = value;
base.OnPropertyChanged("Command");
}
}
public IList<Menu> Children
{
get { return _children; }
set
{
_children = value;
}
}
#endregion
}
Try something like this:
public class MenuItemViewModel
{
public MenuItemViewModel()
{
this.MenuItems = new List<MenuItemViewModel>();
}
public string Text { get; set; }
public IList<MenuItemViewModel> MenuItems { get; private set; }
}
Assume that your DataContext has a property called MenuItems which is a list of MenuItemViewModel. Something like this should work, then:
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:self="clr-namespace:WpfApplication1"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<HierarchicalDataTemplate DataType="{x:Type self:MenuItemViewModel}"
ItemsSource="{Binding Path=MenuItems}">
<ContentPresenter Content="{Binding Path=Text}" />
</HierarchicalDataTemplate>
</Window.Resources>
<DockPanel>
<Menu DockPanel.Dock="Top" ItemsSource="{Binding Path=MenuItems}" />
<Grid />
</DockPanel>
</Window>
This should get you where you are going
<UserControl x:Class="WindowsUI.Views.Default.MenuView"
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"
xmlns:ViewModels="clr-namespace:WindowsUI.ViewModels"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Header" Value="{Binding Path=DisplayName}"/>
<Setter Property="Command" Value="{Binding Path=Command}"/>
</Style>
<HierarchicalDataTemplate
DataType="{x:Type ViewModels:MenuItemViewModel}"
ItemsSource="{Binding Path=Items}">
</HierarchicalDataTemplate>
</UserControl.Resources>
<Menu DockPanel.Dock="Top" ItemsSource="{Binding Path=Items}"/>
Note that in my example, my menu Item has a property of type ICommand called Command.
This solution doesn't need any code in code behind and that makes it simpler solution.
<Menu>
<MenuItem ItemsSource="{Binding Path=ChildMenuItems}" Header="{Binding Path=Header}">
<MenuItem.Resources>
<HierarchicalDataTemplate DataType="{x:Type vm:MenuItemViewModel}" ItemsSource="{Binding ChildMenuItems}">
<MenuItem Header="{Binding Path=Header}" Command="{Binding Path=Command}"/>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type vm:SeparatorViewModel}">
<Separator>
<Separator.Template>
<ControlTemplate>
<Line X1="0" X2="1" Stroke="Black" StrokeThickness="1" Stretch="Fill"/>
</ControlTemplate>
</Separator.Template>
</Separator>
</DataTemplate>
</MenuItem.Resources>
</MenuItem>
</Menu>
And MenuItem is represented as:
public class MenuItemViewModel : BaseViewModel
{
/// <summary>
/// Initializes a new instance of the <see cref="MenuItemViewModel"/> class.
/// </summary>
/// <param name="parentViewModel">The parent view model.</param>
public MenuItemViewModel(MenuItemViewModel parentViewModel)
{
ParentViewModel = parentViewModel;
_childMenuItems = new ObservableCollection<MenuItemViewModel>();
}
private ObservableCollection<MenuItemViewModel> _childMenuItems;
/// <summary>
/// Gets the child menu items.
/// </summary>
/// <value>The child menu items.</value>
public ObservableCollection<MenuItemViewModel> ChildMenuItems
{
get
{
return _childMenuItems;
}
}
private string _header;
/// <summary>
/// Gets or sets the header.
/// </summary>
/// <value>The header.</value>
public string Header
{
get
{
return _header;
}
set
{
_header = value; NotifyOnPropertyChanged("Header");
}
}
/// <summary>
/// Gets or sets the parent view model.
/// </summary>
/// <value>The parent view model.</value>
public MenuItemViewModel ParentViewModel { get; set; }
public virtual void LoadChildMenuItems()
{
}
}
The concrete MenuItems can be either instantiated directly or you could make your own SubTypes through inheritance.
I know this is an old post but I need this plus how to bind Commands.
As to Guge's question on how to bind Commands:
VMMenuItems is a property in my view model class of type
ObservableCollection<Menu>
and Menu is the class defined above. The MenuItem's Command Property is being bound to the Command Property of the Menu class.
In my view model class
Menu.Command = _fou
where
private ICommand _fou;
The xaml
<ListView.ContextMenu>
<ContextMenu ItemsSource="{Binding Path=VMMenuItems}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding Command}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</ListView.ContextMenu>
If you're wondering how to do separators it's really quite easy.
The code below is part of my ViewModel. Since XAML uses reflection all I need to do is to return 'object' which can be a MenuItemViewModel, Separator, or (if for some wierd reason I needed to) an actual MenuItem.
I'm using yield to dynamically generate the items because it just seems to read better for me. Even though I'm using yield - if the items change I still need to raise a PropertyChanged event for "ContextMenu" as usual but I don't unnecessarily generate the list until it's needed.
public IEnumerable<object> ContextMenu
{
get
{
// ToArray() needed or else they get garbage collected
return GetContextMenu().ToArray();
}
}
public IEnumerable<object> GetContextMenu()
{
yield return new MenuItemViewModel()
{
Text = "Clear all flags",
};
// adds a normal 'Separator' menuitem
yield return new Separator();
yield return new MenuItemViewModel()
{
Text = "High Priority"
};
yield return new MenuItemViewModel()
{
Text = "Medium Priority"
};
yield return new MenuItemViewModel()
{
Text = "Low Priority"
};
yield break;
}
I have a traditional form layout with a menu bar at the top and status bar at the bottom. When the user selects a menu item, the space in-between (the form's entire remaining client area) gets replaced with a user control - think of an SDI app that can host multiple types of documents.
If you know of a better way to go about this, please chime in. For now, I'm trying to get it to work in a very simplified version with a ContentControl, but I cannot get it to update the screen when its DataContext is set.
Here's the very simple code for ViewModelA. ViewModelB is identical, except for the Bs.
namespace Dynamic_ContentControl
{
public class ViewModelA: ViewModelBase
{
public ViewModelA()
{
DisplayName = "This is A";
}
}
}
The main window is very simple. It basically declares a property to hold the view model of the hosted control and exposes two commands to assign view models A or B.
namespace Dynamic_ContentControl
{
public class MainViewModel: ViewModelBase
{
private ViewModelBase clientContent = null;
public ICommand ShowA { get; private set; }
public ICommand ShowB { get; private set; }
public ViewModelBase ClientContent {
get
{
return clientContent;
}
private set
{
clientContent = value;
OnPropertyChanged("ClientContent");
}
}
public MainViewModel()
{
ShowA = new RelayCommand((obj) =>
{
ClientContent = new ViewModelA();
});
ShowB = new RelayCommand((obj) =>
{
ClientContent = new ViewModelB();
});
}
}
}
Finally, the XAML declares a ContentControl and sets its ContentTemplate to a DataTemplate called ClientAreaTemplate, whose ContentPresenter points to another DataTemplate, named TextBlockLayout:
<Window x:Class="Dynamic_ContentControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Dynamic_ContentControl"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<vm:MainViewModel />
</Window.DataContext>
<Window.Resources>
<DataTemplate x:Key="TextBlockLayout">
<TextBlock Text="{Binding Path=DisplayName}" />
</DataTemplate>
<DataTemplate x:Key="ButtonLayout">
<Button Content="{Binding Path=DisplayName}" />
</DataTemplate>
<DataTemplate x:Key="CheckBoxLayout">
<CheckBox Content="{Binding Path=DisplayName}" />
</DataTemplate>
<DataTemplate x:Key="ClientAreaTemplate">
<ContentPresenter x:Name="ContentArea" ContentTemplate="{StaticResource ResourceKey=TextBlockLayout}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=DataContext}"
Value="{x:Type vm:ViewModelB}">
<Setter TargetName="ContentArea"
Property="ContentTemplate"
Value="{StaticResource ResourceKey=ButtonLayout}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=DataContext}"
Value="{x:Type vm:ViewModelB}">
<Setter TargetName="ContentArea"
Property="ContentTemplate"
Value="{StaticResource ResourceKey=CheckBoxLayout}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Window.Resources>
<Grid>
<Button Content="Show A"
Command="{Binding Path=ShowA}"
HorizontalAlignment="Left"
Margin="10,10,0,0"
VerticalAlignment="Top"
Width="75" />
<Button Content="Show B"
Command="{Binding ShowB}"
HorizontalAlignment="Left"
Margin="90,10,0,0"
VerticalAlignment="Top"
Width="75" />
<Label Content="{Binding Path=ClientContent.DisplayName}"
HorizontalAlignment="Left"
Margin="170,8,0,0"
VerticalAlignment="Top" />
<ContentControl DataContext="{Binding Path=ClientContent}"
Content="{Binding}"
ContentTemplate="{StaticResource ResourceKey=ClientAreaTemplate}"
HorizontalAlignment="Left"
Margin="10,37,0,0"
VerticalAlignment="Top"
Height="198"
Width="211" />
</Grid>
</Window>
Expected behaviour
When the screen opens, I want TextBoxLayout to display. If the user then clicks on one of the two buttons, it should load either a ButtonLayout or CheckBoxLayout, depending on the actual runtime type of the view model that is assigned.
Actual behaviour
The screen opens with the TextBoxLayout loaded, but it never changes to another type as I click the buttons.
I think the problem is the way the DataTrigger tries to compare with the type, but there are no binding messages at all the Output window.
In this situation, you need to use DataTemplateSelector:
Provides a way to choose a DataTemplate based on the data object and the data-bound element.
Here is a version of dynamic DataTemplateSelector which returns a desired DataTemplate depending on the type:
/// <summary>
/// Provides a means to specify DataTemplates to be selected from within WPF code
/// </summary>
public class DynamicTemplateSelector : DataTemplateSelector
{
/// <summary>
/// Generic attached property specifying <see cref="Template"/>s
/// used by the <see cref="DynamicTemplateSelector"/>
/// </summary>
/// <remarks>
/// This attached property will allow you to set the templates you wish to be available whenever
/// a control's TemplateSelector is set to an instance of <see cref="DynamicTemplateSelector"/>
/// </remarks>
public static readonly DependencyProperty TemplatesProperty =
DependencyProperty.RegisterAttached("Templates", typeof(TemplateCollection), typeof(DataTemplateSelector),
new FrameworkPropertyMetadata(new TemplateCollection(), FrameworkPropertyMetadataOptions.Inherits));
/// <summary>
/// Gets the value of the <paramref name="element"/>'s attached <see cref="TemplatesProperty"/>
/// </summary>
/// <param name="element">The <see cref="UIElement"/> who's attached template's property you wish to retrieve</param>
/// <returns>The templates used by the givem <paramref name="element"/>
/// when using the <see cref="DynamicTemplateSelector"/></returns>
public static TemplateCollection GetTemplates(UIElement element)
{
return (TemplateCollection)element.GetValue(TemplatesProperty);
}
/// <summary>
/// Sets the value of the <paramref name="element"/>'s attached <see cref="TemplatesProperty"/>
/// </summary>
/// <param name="element">The element to set the property on</param>
/// <param name="collection">The collection of <see cref="Template"/>s to apply to this element</param>
public static void SetTemplates(UIElement element, TemplateCollection collection)
{
element.SetValue(TemplatesProperty, collection);
}
/// <summary>
/// Overriden base method to allow the selection of the correct DataTemplate
/// </summary>
/// <param name="item">The item for which the template should be retrieved</param>
/// <param name="container">The object containing the current item</param>
/// <returns>The <see cref="DataTemplate"/> to use when rendering the <paramref name="item"/></returns>
public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
{
//This should ensure that the item we are getting is in fact capable of holding our property
//before we attempt to retrieve it.
if (!(container is UIElement))
return base.SelectTemplate(item, container);
//First, we gather all the templates associated with the current control through our dependency property
TemplateCollection templates = GetTemplates(container as UIElement);
if (templates == null || templates.Count == 0)
base.SelectTemplate(item, container);
//Then we go through them checking if any of them match our criteria
foreach (var template in templates)
//In this case, we are checking whether the type of the item
//is the same as the type supported by our DataTemplate
if (template.Value.IsInstanceOfType(item))
//And if it is, then we return that DataTemplate
return template.DataTemplate;
//If all else fails, then we go back to using the default DataTemplate
return base.SelectTemplate(item, container);
}
}
/// <summary>
/// Holds a collection of <see cref="Template"/> items
/// for application as a control's DataTemplate.
/// </summary>
public class TemplateCollection : List<Template>
{
}
/// <summary>
/// Provides a link between a value and a <see cref="DataTemplate"/>
/// for the <see cref="DynamicTemplateSelector"/>
/// </summary>
/// <remarks>
/// In this case, our value is a <see cref="System.Type"/> which we are attempting to match
/// to a <see cref="DataTemplate"/>
/// </remarks>
public class Template : DependencyObject
{
/// <summary>
/// Provides the value used to match this <see cref="DataTemplate"/> to an item
/// </summary>
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(Type), typeof(Template));
/// <summary>
/// Provides the <see cref="DataTemplate"/> used to render items matching the <see cref="Value"/>
/// </summary>
public static readonly DependencyProperty DataTemplateProperty =
DependencyProperty.Register("DataTemplate", typeof(DataTemplate), typeof(Template));
/// <summary>
/// Gets or Sets the value used to match this <see cref="DataTemplate"/> to an item
/// </summary>
public Type Value
{ get { return (Type)GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } }
/// <summary>
/// Gets or Sets the <see cref="DataTemplate"/> used to render items matching the <see cref="Value"/>
/// </summary>
public DataTemplate DataTemplate
{ get { return (DataTemplate)GetValue(DataTemplateProperty); } set { SetValue(DataTemplateProperty, value); } }
}
Example of using
<local:DynamicTemplateSelector x:Key="MyTemplateSelector" />
<DataTemplate x:Key="StringTemplate">
<TextBlock>
<Run Text="String: " />
<Run Text="{Binding}" />
</TextBlock>
</DataTemplate>
<DataTemplate x:Key="Int32Template">
<TextBlock>
<Run Text="Int32: " />
<Run Text="{Binding}" />
</TextBlock>
</DataTemplate>
<Style x:Key="MyListStyle" TargetType="ListView">
<Setter Property="ItemTemplateSelector" Value="{StaticResource MyTemplateSelector}"/>
<Setter Property="local:DynamicTemplateSelector.Templates">
<Setter.Value>
<local:Templates>
<local:Template Value={x:Type String} DataTemplate={StaticResource StringTemplate}/>
<local:Template Value={x:Type Int32} DataTemplate={StaticResource Int32Template}/>
</local:Templates>
</Setter.Value>
</Setter>
</Style>