I am using Prism. I have a view(DetailMainUC.xaml) which hold many other views in the following way.
<UserControl.Resources>
<DataTemplate x:Key="View1Template" DataType="{x:Type vm:DetailMainUCViewModel}">
<local:HomeUC />
</DataTemplate>
<DataTemplate x:Key="View2Template" DataType="{x:Type vm:DetailMainUCViewModel}">
<local:WalkAwayBehaviorUC />
</DataTemplate>
<DataTemplate x:Key="View3Template" DataType="{x:Type vm:DetailMainUCViewModel}">
<local:WakeUpOnApproachUC />
</DataTemplate>
<DataTemplate x:Key="View4Template" DataType="{x:Type vm:DetailMainUCViewModel}">
<local:NoLockOnPresenceUC />
</DataTemplate>
<DataTemplate x:Key="View5Template" DataType="{x:Type vm:DetailMainUCViewModel}">
<local:PeekDimmingUC />
</DataTemplate>
<DataTemplate x:Key="View6Template" DataType="{x:Type vm:DetailMainUCViewModel}">
<local:SettingsUC />
</DataTemplate>
</UserControl.Resources>
<Grid>
<ContentControl Grid.Column="1" Grid.Row="1" Content="{Binding }">
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding SwitchView}" Value="Home">
<Setter Property="ContentTemplate" Value="{StaticResource View1Template}" />
</DataTrigger>
<DataTrigger Binding="{Binding SwitchView}" Value="Walk Away Behaviour">
<Setter Property="ContentTemplate" Value="{StaticResource View2Template}"></Setter>
</DataTrigger>
<DataTrigger Binding="{Binding SwitchView}" Value="Wake up on approach">
<Setter Property="ContentTemplate" Value="{StaticResource View3Template}" />
</DataTrigger>
<DataTrigger Binding="{Binding SwitchView}" Value="No lock on presence">
<Setter Property="ContentTemplate" Value="{StaticResource View4Template}" />
</DataTrigger>
<DataTrigger Binding="{Binding SwitchView}" Value="Peeking and Dimming">
<Setter Property="ContentTemplate" Value="{StaticResource View5Template}" />
</DataTrigger>
<DataTrigger Binding="{Binding SwitchView}" Value="Settings">
<Setter Property="ContentTemplate" Value="{StaticResource View6Template}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</Grid>
Now I am using event aggregator to communicate between different view models in other regions. My view model for the above usercontrol is as follows:
public DetailMainUCViewModel(IEventAggregator eventAggregator, HomeViewModel homeViewModel)
{
this.HomeVM = homeViewModel;
this._eventAggregator = eventAggregator;
HomeVM.Initialization(this._eventAggregator);
this._eventAggregator.GetEvent<MenuClickEvent>().Subscribe(GetMenuName);
SwitchView = "Home";
}
Now the property HomeVM is of type HomeViewModel which is view model of child DetailMainUCViewModel. Like this there are many other child view models are there. Now my problem is I am seeing constructor of HomeViewModel is getting called twice and same is happening for all other child view models.
the main problem I am facing is while child view models are getting called second times , the eventAggregator is becoming null.
DetailsMainUCViewModel is parent viewmodel and HomeViewModel is child viewmodel. DetailMainUC.xaml holds the HomeUC.xaml in the way as mentioned in code section.
I have written the below mentioned code too
<UserControl.DataContext>
<vm:HomeViewModel></vm:HomeViewModel>
</UserControl.DataContext>
I suspect as I am using two places to attach datacontext so my viewmodel is getting called two times.
in
1.HomeUC.xaml
2.Parent usercontrol DetailMainUC.xaml
But I am unable to remove them as it is needed.
My class design is DetailMainUCViewModel is parent viewmodel and HomeViewModel is child viewmodel. DetailMainUC.xaml is parent view and HomeUC.xaml is child view. at the beginning the code snippet which I have given is from DetailMainUC.xaml (which holds all child usercontrols)
apart from this I am initializing my DetailMainUC.xaml in module where I have implemented IModule interface.
public class STDetailOptionsModule : IModule
{
private readonly IRegionManager _regionManager;
public STDetailOptionsModule(IRegionManager regionManager)
{
this._regionManager = regionManager;
}
public void OnInitialized(IContainerProvider containerProvider)
{
_regionManager.RegisterViewWithRegion(RegionNames.DetailOptionsRegion, typeof(DetailMainUC));
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
}
}
regards
If I have understood your question properly then I can say that you are using multiple usercontrols in same usercontrol to navigate by virtue of triggers.
This approach is not correct.
Basically you want a navigation between various usercontorls.
Let me explain how to do this in Prism.
this.regionManager.RequestNavigate("your region name where the usercontrol will be put ",
navigationPath, NavigationCompleted);
NavigationCompleted is a call back method:
navigationPath: is the usercontrol path which you want to place in a specified region.
Hope it is clear
As you said you are new to prism so let me explain vividly.
As I can see you have Views (WalkAwayBehaviorUC, WakeUpOnApproachUC, NoLockOnPresenceUC,PeekDimmingUC, SettingsUC).
Now let me consider these views are in different projects or in same project but you want them to be displayed in your region that is RegionNames.DetailOptionsRegion.
Now to do that , you have to put all navigation logic in a centralized location.
the best centralized location could be shell . So ShellViewModel.cs is the class where you have to write navigation logic.
So create a method like this :
private void NavigationInitialized(string navigationPath)
{
if (string.IsNullOrEmpty(navigationPath))
{
throw new Exception();//or send proper message to UI
}
this.regionManager.RequestNavigate(RegionNames.DetailOptionsRegion,
navigationPath, NavigationCompleted);
}
private void NavigationCompleted(NavigationResult navigationResult)
{
}
Now the question is how to get this navigation name in ShellViewModel.cs.
The solution is EventAggregator.
In the place where you want the usercontrol should navigate write.
this.eventAggregator.GetEvent<Navigated>().Publish("WalkAwayBehaviorUC");
now the Navigated event class should be like :
public class Navigated : PubSubEvent<string>
{
}
Now you have to subscribe this Navigated Event in ShellViewModel.cs
So in constructor of ShellViewModel.cs you have to write:
this.eventAggregator.GetEvent<Navigated>().Subscribe(NavigationInitialized);
Related
I am working on a WPF app and currently I have an ItemsControl bound up to my View Model ObservableCollection and I have a DataTemplate that uses a UserControl to render the items on canvas. Can you use multiple User Controls and then switch which one is used based on an Enum? Another way to look it is to either create a Button or a TextBox for the item in the ObservableCollection based on an Enum.
You can select the data template for an item using a custom DataTemplateSelector. Assume we have the following:
public enum Kind
{
Button, TextBox,
}
public class Data
{
public Kind Kind { get; set; }
public string Value { get; set; }
}
Your data template selector might then look like this:
public class MyTemplateSelector : DataTemplateSelector
{
public DataTemplate ButtonTemplate { get; set; }
public DataTemplate TextBoxTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
Data data = (Data)item;
switch (data.Kind)
{
case Kind.Button:
return ButtonTemplate;
case Kind.TextBox:
return TextBoxTemplate;
}
return base.SelectTemplate(item, container);
}
}
In XAML, declare templates for all the cases you want to cover, in this case buttons and text boxes:
<Window.Resources>
<ResourceDictionary>
<DataTemplate x:Key="ButtonTemplate" DataType="local:Data">
<Button Content="{Binding Value}" />
</DataTemplate>
<DataTemplate x:Key="TextBoxTemplate" DataType="local:Data">
<TextBox Text="{Binding Value}" />
</DataTemplate>
</ResourceDictionary>
</Window.Resources>
Finally, have your ItemsControl create an instance of your custom template selector, initializing its two DataTemplateproperties from the above data templates:
<ItemsControl>
<ItemsControl.ItemTemplateSelector>
<local:MyTemplateSelector
ButtonTemplate="{StaticResource ButtonTemplate}"
TextBoxTemplate="{StaticResource TextBoxTemplate}"/>
</ItemsControl.ItemTemplateSelector>
<ItemsControl.Items>
<local:Data Kind="Button" Value="1. Button" />
<local:Data Kind="TextBox" Value="2. TextBox" />
<local:Data Kind="TextBox" Value="3. TextBox" />
<local:Data Kind="Button" Value="4. Button" />
</ItemsControl.Items>
</ItemsControl>
(In real life, set the ItemsSource instead of declaring the items inline, as I did.)
For completeness: To access your C# classes you need to set up the namespace, e.g.,
xmlns:local="clr-namespace:WPF"
Another possible quick solution is to use Data Triggers:
<ContentControl>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Setter Property="Content"
Value="{StaticResource YourDefaultLayout}" />
<Style.Triggers>
<DataTrigger Binding="{Binding YourEnumVMProperty}"
Value="{x:Static local:YourEnum.EnumValue1}">
<Setter Property="Content"
Value="{StaticResource ContentForEnumValue1}" />
</DataTrigger>
<DataTrigger Binding="{Binding YourEnumVMProperty}"
Value="{x:Static local:YourEnum.EnumValue2}">
<Setter Property="Content"
Value="{StaticResource ContentForEnumValue2}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
You could also define the template of a whole control using a trigger setter.
I prefer this because there is no need to define all the DataTemplateSelector stuff etc.
I am currently working on a project for work. I am seeking an outside design opinion, as well as some general information on the issue I am faced with.
We have a MainWindow.xaml file that is located in the root directory of the project. In this main window is some design and logic for some collapsing stack panels, ribbon toolbar, etc.
So far the idea is to include a different in each stack panel to help make the code neat. The views are located in a 'Views' folder. So just to be clear, the MainWindow.xaml and other views ARE NOT in the same directory. This is open to change, if necessary.
So here is my question/issue: We have a Window ('A'), a main panel with a collapsable stack panel with some information ('B') that is contained in window 'A'. Then there is another stack panel to manage the contents in 'B', (collapse/visisble) ('C').
'A' contains a toggle button to show/collapse 'B'.
'B' contains a button to show/collapse 'C'.
'C' contains a button to show/collapse itself, 'C'.
'C' should have its logic all contain within a view, so the MainWindow ('A') should have a simple tag:
<StackPanel Style="{StaticResource FrameGradient}" Tag="{Binding ElementName=ToggleButton}">
<view:Content></view:Content>
</StackPanel>
Currently, the bindings for toggling the buttons within 'A' are in the styling. The In this case FrameGradient has triggers like so:
<Style x:Key="FrameGradient" TargetType="{x:Type StackPanel}">
//Setter properties
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Tag.IsChecked, RelativeSource={RelativeSource Self}}" Value="False">
<Setter Property="StackPanel.Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Tag.IsChecked, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="StackPanel.Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
Is it possible to, within the 'Content' View to TOGGLE the panel, 'C', which is NOT within the view? I feel like I am missing a core idea of XAML here. I found a 'cheap' work around which is to place the 'Close' button from the Content View outside of the tags, but then that leads to styling issues and I feel like I shouldn't have to do something silly like that. Again, the idea is that the toggle button for Stack Panel 'C' is contained within another view and I want to be able to toggle it from another view.
I apologize if I am not clear enough, I will provide whoever asks with more information if required here.
UPDATE
I have some time to actually add the code I am using so that this might make more sense.
MainWindow.xaml - Logic for Filter panel (Located in root)
<StackPanel Grid.Row="1" Grid.Column="4" Visibility="Collapsed" Style="{StaticResource FrameGradient}">
<Grid x:Name="FilterContentGrid">
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<view:Filters></view:Filters>
</Grid>
</StackPanel>
Filters.xaml - Logic for Filters view (Located in /Views)
The button within the file that needs to Collapse the above StackPanel.
<Button x:Name="FilterManagementCloseButton" Content="CLOSE"></Button>
Theme.Xaml - Logic for all styling (Located in root, along with MainWindow.xaml and App.xaml)
Button Styling
<Style x:Key="FilterManagementCloseButton" TargetType="Button">
<Setter Property="Padding" Value="10,5,20,3" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Tag.IsChecked, RelativeSource={RelativeSource AncestorType={x:Type Local:MainWindow}}}" Value="True">
<Setter Property="StackPanel.Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
And finally, the FrameGradient Styling also located in Theme.xaml
<Style x:Key="FrameGradient" TargetType="{x:Type StackPanel}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Tag.IsChecked, RelativeSource={RelativeSource Self}}" Value="False">
<Setter Property="StackPanel.Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Tag.IsChecked, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="StackPanel.Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
SO, I hope this makes things more clear. I want the CLOSE button within Filters.xaml to COLLAPSE the stackpanel that is located in MainWindow. I realize this code is a mess at the moment.
Is it possible to, within the 'Content' View to TOGGLE the panel, 'C',
which is NOT within the view?
Create a shared VM which each other VM will have a property for which it can access. This VM can be loaded with during initialization of the other VMs. To allow for changes to happen put INotifyProperty(ies) on the shared VM which will then flag the desired logic across all views. Finally bind the target control(s) to your datacontext as normal except sub path into the shared VM target's property.
Hence when one view toggles (two way binding) a shared property it is reflected on the view of the target panel.
Update Example
The idea here is that one creates a viewmodel for the AppPage. That VM will hold generic flags which are shared across all viewmodels. Each subsequently created ViewModel will have a reference to the AppPage's viewmodel.
The example below is a mainpage where the AppVM contains a flag which informs the mainpage whether a login is in process. If it is and that value is true then a bound button on the mainpage will be enabled.
Subsequently the mainpage can override the appvm and put a new value within that flag by a bounded checkbox that can in-directly change whether the button is enabled; thus changing the flag for all other VMs in the process.
Here is the Mainpage VM, for this example I simply create the AppVM, but it could be passed in, or gotten from a static reference elsewhere. NOTE also how I don't care when AVs (appVM) property changes; it is not required for this example (we are not binding anything to AppVM, just its properties which need to be monitored).
public class MainVM : INotifyPropertyChanged
{
public AppVM AV { get; set; }
public MainVM()
{
AV = new AppVM() { LoginInProcess = true };
}
}
Here is the AppVm
public class AppVM : INotifyPropertyChanged
{
private bool _LoginInProcess;
public bool LoginInProcess
{
get { return _LoginInProcess; }
set { _LoginInProcess = value; OnPropertyChanged(); }
}
}
Here is MainPage's Xaml where the datacontext has been set to an instance of MainVM:
<StackPanel Orientation="Vertical">
<CheckBox Content="Override"
IsChecked="{Binding AV.LoginInProcess, Mode=TwoWay}"/>
<Button Content="Login"
IsEnabled="{Binding AV.LoginInProcess}"
Width="75" />
</StackPanel>
I base the MVVM off of my blog article Xaml: ViewModel Main Page Instantiation and Loading Strategy for Easier Binding which explains the other missing items of this example such as the mainpage's datacontext loading.
You can use RelativeSource Bindings to bind from child views to properties in parent view models. Let's say you have a ToggleButton in MainWindow.xaml that is data bound to a property named IsChecked, which is declared in the object that is data bound to the MainWindow DataContext property. You could data bind to that same property from any child view with a RelativeSource Binding, like this:
<Style x:Key="FrameGradient" TargetType="{x:Type StackPanel}">
<Setter Property="StackPanel.Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=DataContext.IsChecked, RelativeSource={
RelativeSource AncestorType={x:Type Local:MainWindow}}}" Value="False">
<Setter Property="StackPanel.Visibility" Value="Collapsed" />
</DataTrigger>
<!-- Note that there is no need for two Triggers here -->
<!-- One Setter and one Trigger is enough -->
</Style.Triggers>
</Style>
I have a tree view like below added mousedouble click
<TreeView
Grid.Row="0"
Name="tvTopics"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
MouseDoubleClick="tvTopics_MouseDoubleClick"
ItemsSource="{Binding TierOneItems}"
SelectedItemChanged="treeView1_SelectedItemChanged">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding Topic.IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
<Trigger Property="IsExpanded" Value="True">
<Setter Property="IsSelected" Value="True" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
on my code behind
private void tvTopics_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
TreeView tv = sender as TreeView;
if (tv.SelectedItem is TopicTreeItemViewModel)
{
Model.SelectedTopic = ((TopicTreeItemViewModel)tv.SelectedItem).Topic;
}
}
here i am trying to pass my "topic" value to my view model but i have no idea how to pass or call my view model method.
public class TopicTreeViewModel : NotificationObject, ITopicTreeViewModel
{
[ImportingConstructor]
public TopicTreeViewModel(IGatewayService storyService, IEventAggregator eventAggregator)
{
this.storyService = storyService;
this.eventAggregator = eventAggregator;
this.AddTopicCommand = new DelegateCommand<object>(this.AddTopic);
Helper.SubscriptionTokenList_LocationSearch.Add(this.eventAggregator.GetEvent<LocationSearchEvent>().Subscribe(OnLocationSearch, ThreadOption.UIThread));
Helper.SubscriptionTokenList_SubjectSearch.Add(this.eventAggregator.GetEvent<SubjectSearchEvent>().Subscribe(OnSubjectSearch, ThreadOption.UIThread));
}
public void MouseDoubleClick(Topic topic)
{
if (topic != null && topic is Topic)
{
switch (this.searchType)
{
case SearchType.Location:
this.eventAggregator.GetEvent<AddLocationEvent>().Publish((Topic)topic);
break;
case SearchType.Subject:
this.eventAggregator.GetEvent<AddSubjectEvent>().Publish((Topic)topic);
break;
}
}
}
And the interface connect between view and view model
public interface ITopicTreeViewModel
{
ReadOnlyCollection<TopicTreeItemViewModel> TierOneItems { get; }
ICommand SearchCommand { get; }
string SearchText { get; set; }
Topic SelectedTopic { get; set; }
}
All im trying to do here is passing the topic value to my view model when the mousedouble click event triggered.
I have no idea how to pass or bind this value. any help much appreciated.
When using Prism and MVVM in particular, it is reccomended to add the minimal code behind implementation as possible. Therefore, every logic or action performed would be handled directly into the ViewModel.
Instead of handling the event on the View's Code Behind, you should bind the MouseDoubleClick event to a Delegate Command in the ViewModel. So, in order to achieve this, you would need to set the proper ViewModel as the DataContext of the View. This way, Binding would be resolved through the DataContext implementation.
The following MSDN Prism Guide chapter would be helpful to understand the interaction between View and ViewModel:
Implementing the MVVM pattern
Advance MVVM scenarios
In addition, you could take a look at the MVVM Prism QuickStart and undestand how the Binding to the View-ViewModel interaction is implemented.
I hope this helped, Regards.
So I have some code similar to the following: (Forgive any typos-- I tried to simplify in the SO editor for the post)
<my:CustomContentControl>
<my:CustomContentControl.Style>
<Style TargetType="{x:Type my:CustomContentControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=CurrentView}" Value="MyCustomView">
<Setter Property="Content">
<Setter.Value>
<my:CustomView DataContext="{Binding DataContextForMyCustomView"/>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</m:CustomContentControl.Style>
</my:CustomContentControl>
The problem is that whenever the DataTrigger occurs, the setter does set the Content property to my:CustomView, but it does not bind DataContext. If I move the same code outside of the trigger the DataContext binding works just fine.
Any ideas? If this is a limitation of some sorts, is there any work around?
Update:
I received the following error in the output window:
System.Windows.Data Error: 3 : Cannot find element that provides DataContext. BindingExpression:Path=DataContextForMyCustomView; DataItem=null; target element is 'CustomView' (Name='customView'); target property is 'DataContext' (type 'Object')
The error you posted makes it sound like your custom control is in an object that doesn't have a DataContext, such as a DataGridColumn.Header.
To get around that, you can create a Freezeable object in your .Resources containing the binding you're looking for, then bind your my:CustomView.DataContext to that object
<my:CustomContentControl.Resources>
<local:BindingProxy x:Key="proxy"
Data="{Binding DataContextForMyCustomView, ElementName=MyControl}" />
</my:CustomContentControl.Resources>
...
<my:CustomView DataContext="{Binding Source={StaticResource proxy}}"/>
Here's the code for a sample Freezable object copied from here:
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object),
typeof(BindingProxy), new UIPropertyMetadata(null));
}
Also, you really should use ContentTemplate instead of Content to avoid an exception if more than one object applies that style :)
I solved a similar problem by putting the UserControl into the resources and then changing the Content with that.
e.g. from my own code (different names, same concept)
<ContentControl Grid.Column="1"
Margin="7,0,7,0">
<ContentControl.Resources>
<mapping:Slide11x4MappingView x:Key="Slide11X4MappingView" DataContext="{Binding MappingViewModel}"/>
<mapping:MicrotubeMappingView x:Key="MicrotubeMappingView" DataContext="{Binding MappingViewModel}"/>
</ContentControl.Resources>
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding Acquirer.Sorter.TrayType}" Value="{x:Static mapping:TrayType.SLIDES11X4}">
<Setter Property="Content" Value="{StaticResource Slide11X4MappingView}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Acquirer.Sorter.TrayType}" Value="{x:Static mapping:TrayType.VIALS}">
<Setter Property="Content" Value="{StaticResource MicrotubeMappingView}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
MSDN says "Gets or sets an arbitrary object value that can be used to store custom information about this element." which means I can store anything I want in this property.
But if you bind to this property (with property of type String having a value say "XYZ") and use it in Trigger conditions it doesn't work!
<Trigger Property="Tag" Value="XYZ">
<Setter Property="Background" Value="Red" />
</Trigger>
It does not set the background red. You can try and assume myElement to be a TextBlock! Why is it like this?
Tag has no special functionality in WPF.
This works for me:
<TextBlock Tag="{Binding Data}"
x:Name="tb">
<TextBlock.Style>
<Style>
<Style.Triggers>
<Trigger Property="TextBlock.Tag"
Value="XYZ">
<Setter Property="TextBlock.Background"
Value="Lime" />
</Trigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
And setting the Data object property to "XYZ" in an event.
The Tag is a construct held over from Winforms days (and possibly there from before that!). It was used as a convenient place to associate an object with a UI element, such as a FileInfo with a Button, so in the Button's event handler you could simply take the event sender, cast it to a Button, then cast the Tag value to a FileInfo and you have everything you need about the file you want to open.
There is one situation, however, where I've found the Tag is useful in WPF. I've used it as a holding spot that can be accessed by a ContextMenu MenuItem, which can't use the normal RelativeSource bindings you'd use to traverse the visual tree.
<ListBox.ItemContainerStyle>
<Style
TargetType="ListBoxItem">
<Setter
Property="Tag"
Value="{Binding ElementName=TheUserControlRootElement}" />
<Setter
Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem
Header="_Remove"
ToolTip="Remove this from this list"
CommandParameter="{Binding DataContext, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
Command="{Binding PlacementTarget.Tag.Remove, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
From the ContextMenu, I cannot access the Remove command which is defined in the UserControl class where this snippet is defined. But I can bind the root to the Tag of the ListBoxItem, which I can access via the ContextMenu.PlacementTarget property. The same trick can be used when binding within a ToolTip, as the same limitations apply.
MainWindow.xaml:
<Window x:Class="wpftest.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">
<Grid>
<TextBlock x:Name="test" MouseDown="test_MouseDown"
Tag="{Binding TestProperty}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<Trigger Property="Tag" Value="XYZ">
<Setter Property="Background" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</Window>
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new TestViewModel();
}
private void test_MouseDown(object sender, MouseButtonEventArgs e)
{
((TestViewModel)DataContext).TestProperty = "XYZ";
}
private sealed class TestViewModel : INotifyPropertyChanged
{
private string _testPropertyValue;
public string TestProperty
{
get { return _testPropertyValue; }
set
{
_testPropertyValue = value;
var handler = PropertyChanged;
if(handler != null)
handler(this, new PropertyChangedEventArgs("TestProperty"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
Updated: Tag property now is bound to TestProperty.