The objective is to swap the control at runtime based on a property in my ViewModel, and have the displayed control be have a binding that updates the properties in the ViewModel. I started by creating the following Style in the View.xaml:
<UserControl.Resources>
<Style x:Key="DisplayTextOrButton" TargetType="{x:Type ContentControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding TextNotButton}" Value="True">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBox Content="{Binding SomeText}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding TextNotButton}" Value="False">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Button Content="{Binding ButtonText}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
Note that the bindings LabelText and ButtonText are the bindings to the properties in the ViewModel.
Then further on in the View.xaml I have the following:
<ContentControl Content="{Binding TextNotButton}"
Style="{StaticResource DisplayTextOrButton}">
</ContentControl>
Finally, the ViewModel.cs has the following properties:
private bool textNotButton;
public bool TextNotButton
{
get => this.textNotButton;
set
{
this.textNoButton = value;
this.OnPropertyChanged("TextNotButton");
}
}
private string someText;
public string SomeText
{
get => this.someText;
set
{
this.someText = value;
this.OnPropertyChanged("SomeText");
}
}
private string buttonText;
public string ButtonText
{
get => this.buttonText;
set
{
this.buttonText = value;
this.OnPropertyChanged("ButtonText");
}
}
The style works well for swapping between the label and the button, but changing the text in the TextBox does not update the property in the ViewModel, and the Button's text is empty (I imagine because the binding hasn't worked)
I believe this is because the style is a static resource so the bindings SomeText and ButtonText in the style aren't actually the bindings in the ViewModel, but I'm not sure how to pass the reference of the other properties into the style. Or even if that's a thing. I'm pretty new to XAML so not sure on how to handle this
Related
I am trying to bind some values to ToolTipService.ShowDuration and some other ToolTip's properties in a CellStyle of DataGridTextColumn.
Normaly, I am doing something like this:
<UserControl
...namespace declarations...>
<UserControl.Resources>
<mycontrols:BindingProxy x:Key="proxy" Data="{Binding MySettings}"/>
</UserControl.Resources>
<DataGrid>
<DataGridTextColumn
Binding="{Binding SomeBinding}">
<DataGridTextColumn.CellStyle>
<Style
TargetType="DataGridCell"
BasedOn="{StaticResource ResourceKey={x:Type DataGridCell}}">
<Setter
Property="ToolTipService.ShowDuration"
Value="{Binding Data.ToolTipDuration, Source={StaticResource proxy}}"/>
<Setter Property="ToolTip">
<Setter.Value>
<TextBlock
Text="{Binding SomeBinding}"
MaxWidth="{Binding Data.ToolTipMaxWidth, Source={StaticResource proxy}}"
TextWrapping="Wrap" TextTrimming="CharacterEllipsis"/>
</Setter.Value>
</Setter>
</Style>
</DataGridTextColumn.CellStyle>
</DataGridTextColumn>
</DataGrid>
</UserControl>
Since ToolTip has it's own visual tree, I am using binding proxy like this:
public class BindingProxy : Freezable
{
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
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));
}
Up to this point, all works as expected. But I wanted to re-use this DataGridTextColumn, so I created new file like this:
<DataGridTextColumn
x:Class="Test.MyControls.DataGridLargeTextColumn"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Test.MyControls">
<DataGridTextColumn.CellStyle>
<Style
TargetType="DataGridCell"
BasedOn="{StaticResource ResourceKey={x:Type DataGridCell}}">
<Setter
Property="ToolTipService.ShowDuration"
Value="{Binding ToolTipDuration}"/>
<Setter Property="ToolTip">
<Setter.Value>
<TextBlock
Text="{Binding SomeBinding}"
MaxWidth="{Binding ToolTipWidth}"
TextWrapping="Wrap" TextTrimming="CharacterEllipsis"/>
</Setter.Value>
</Setter>
</Style>
</DataGridTextColumn.CellStyle>
</DataGridTextColumn>
With code behinde:
public partial class DataGridLargeTextColumn : DataGridTextColumn
{
public int ToolTipDuration
{
get { return (int)GetValue(ToolTipDurationProperty); }
set { SetValue(ToolTipDurationProperty, value); }
}
public static readonly DependencyProperty ToolTipDurationProperty =
DependencyProperty.Register("ToolTipDuration", typeof(int), typeof(DataGridLargeTextColumn), new UIPropertyMetadata(default(int)));
public string SomeBinding
{
get { return (string)GetValue(SomeBindingProperty); }
set { SetValue(SomeBindingProperty, value); }
}
public static readonly DependencyProperty SomeBindingProperty =
DependencyProperty.Register("SomeBinding", typeof(string), typeof(DataGridLargeTextColumn), new UIPropertyMetadata(default(string)));
public int ToolTipWidth
{
get { return (int)GetValue(ToolTipWidthProperty); }
set { SetValue(ToolTipWidthProperty, value); }
}
public static readonly DependencyProperty ToolTipWidthProperty =
DependencyProperty.Register("ToolTipWidth", typeof(int), typeof(DataGridLargeTextColumn), new UIPropertyMetadata(default(int)));
public DataGridLargeTextColumn()
{
InitializeComponent();
}
}
This does't work because ToolTip has it's own visual tree, but since I have nowhere to put proxy, I don't know how to make it work or if it is even possible. I found this answer, and it seems to be on the right track, however, I tried to implement it like this with no luck:
<Setter
Property="ToolTipService.ShowDuration"
Value="{Binding Path=PlacementTarget.(local:DataGridLargeTextColumn.ToolTipDuration), RelativeSource={RelativeSource Self}}"/>
<Setter Property="ToolTip">
<Setter.Value>
<TextBlock
Text="{Binding Path=PlacementTarget.(local:DataGridLargeTextColumn.SomeBinding), RelativeSource={RelativeSource Self}}"
MaxWidth="{Binding Path=PlacementTarget.(local:DataGridLargeTextColumn.ToolTipWidth), RelativeSource={RelativeSource Self}}"
TextWrapping="Wrap" TextTrimming="CharacterEllipsis"/>
</Setter.Value>
</Setter>
Am I using PlacementTarget wrong? If not, why is it not working, and is there another solution?
UPDATE:
As per Mark's answer, I've modified the DataGridLargeTextColumn's Style:
<Style
TargetType="DataGridCell"
BasedOn="{StaticResource {x:Type DataGridCell}}">
<Setter
Property="ToolTipService.ShowDuration" Value="{Binding Path=PlacementTarget.ToolTipShowDuration, RelativeSource={x:Static RelativeSource.Self}}"/>
<Setter Property="ToolTip">
<Setter.Value>
<ToolTip DataContext="{Binding Path=PlacementTarget, RelativeSource={x:Static RelativeSource.Self}}">
<TextBlock
Text="{Binding DataContext.SomeBinding}"
MaxWidth="{Binding Column.ToolTipWidth}"
TextWrapping="Wrap" TextTrimming="CharacterEllipsis"/>
</ToolTip>
</Setter.Value>
</Setter>
</Style>
And I'm using that control like this:
<UserControl
...namespace declarations...>
<UserControl.Resources>
<mycontrols:BindingProxy x:Key="proxy" Data="{Binding MySettings}"/>
</UserControl.Resources>
<DataGrid>
<DataGrid.Columns>
<mycontrols:DataGridLargeTextColumn
Binding="{Binding SomeBinding}"
ToolTipShowDuration="{Binding Data.ToolTipDuration, Source={StaticResource proxy}}"
ToolTipWidth="{Binding Data.ToolTipMaxWidth, Source={StaticResource proxy}}"/>
</DataGrid.Columns>
</DataGrid>
</UserControl>
Width binding now works like a charm, but there are two problems I still cannot solve:
I cannot get tool tip's duration to bind, I have tried few different approches, but since it's abstract, it cannot be declared explicitly
ToolTip's Text property is set to SomeBinding, which is OK in this particular case, but I want to be able to set it to whatever, so I tried to declare it using DependencProperty from above like this:
Text="{Binding Column.ToolTipText}"
This works OK if i use it with string literal:
<myControls:DataGridLargeTextColumn
Binding="{Binding SomeBinding}"
ToolTipText="12345"
ToolTipShowDuration="{Binding Data.ToolTipDuration, Source={StaticResource proxy}}"
ToolTipWidth="{Binding Data.ToolTipMaxWidth, Source={StaticResource proxy}}"/>
But it doesn't work when I try to bind it, which is what I am trying to achieve:
<myControls:DataGridLargeTextColumn
Binding="{Binding SomeBinding}"
ToolTipText="{Binding SomeOtherPropertyBinding}"
ToolTipShowDuration="{Binding Data.ToolTipDuration, Source={StaticResource proxy}}"
ToolTipWidth="{Binding Data.ToolTipMaxWidth, Source={StaticResource proxy}}"/>
By default your ToolTip's DataContext gets set to the DataContext of the cell. You, however, are trying to bind to dependency properties in the cell's column instead, so you're going to have to change the DataContext to point to the cell itself and then reference DataContext explicitly when you want to access the data and Column when you want to access the DPs in your DataGridLargeTextColumn.
In other words, declare the ToolTip explicitly in addition to its content and set its DataContext, like this:
<Setter Property="ToolTip">
<Setter.Value>
<ToolTip DataContext="{Binding Path=PlacementTarget, RelativeSource={x:Static RelativeSource.Self}}">
<TextBlock
Text="{Binding DataContext.SomeBinding}"
Width="{Binding Column.ToolTipWidth}" />
</ToolTip>
</Setter.Value>
</Setter>
...where Text in this case is binding to the data and Width is binding to the custom column DP.
Alternatively you could also just leave the DataContext as is and instead use the ToolTip's Tag property as a binding proxy to the DataGridLargeTextColumn:
<Setter Property="ToolTip">
<Setter.Value>
<ToolTip Tag="{Binding Path=PlacementTarget.Column, RelativeSource={x:Static RelativeSource.Self}}">
<TextBlock
Text="{Binding SomeBinding}"
Width="{Binding Tag.ToolTipWidth, RelativeSource={RelativeSource AncestorType=ToolTip}}" />
</ToolTip>
</Setter.Value>
</Setter>
I have a ComboBox containing four int values, 1, 2, 3, 4. Below that, I have a GroupBox. What I'm trying to do is display unique content in the GroupBox based on the values of the ComboBox; i.e. maybe a TextBlock or TextBox for 1 and a Button for 2.
Currently, I have four separate GroupBoxes all on top of each other and have the Visibility property of the those GroupBoxes bound to unique bool properties in the ViewModel which I set whenever the SelectedItem of the ComboBox changes. But I feel like there's a way where I can condense this and just keep one GroupBox and fill it with the correct content instead of having four separate ones and four separate IsVisible properties.
<ComboBox ItemsSource="{Binding PackageTypes}" SelectedItem="{Binding SelectedType}" />
<GroupBox /><!--Change content of this based on combobox's selection-->
public class MainViewModel : ViewModelBase
{
private PackageModel_selectedPackageModel;
public PackageModel SelectedPackageModel
{
get => _selectedPackageModel;
set
{
_selectedPackageModel = value;
}
}
public ObservableCollection<int> PackageTypes { get; set; }
public int SelectedType { get; set; }
public MainViewModel()
{
PackageTypes = new ObservableCollection<int>() { 1, 2, 3, 4 };
}
}
You may use a Style with triggers, e.g.:
<ComboBox x:Name="cmb" xmlns:s="clr-namespace:System;assembly=mscorlib">
<s:Int32>1</s:Int32>
<s:Int32>2</s:Int32>
<s:Int32>3</s:Int32>
<s:Int32>4</s:Int32>
</ComboBox>
<GroupBox Header="...">
<GroupBox.Style>
<Style TargetType="GroupBox">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedItem, ElementName=cmb}" Value="1">
<Setter Property="Content">
<Setter.Value>
<TextBlock Text="1..." />
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedItem, ElementName=cmb}" Value="2">
<Setter Property="Content">
<Setter.Value>
<Button Content="2..." />
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</GroupBox.Style>
</GroupBox>
Instead of binding to the SelectedItem property of the ComboBox, you may of course bind to a source property of the view model:
<DataTrigger Binding="{Binding SelectedType}" Value="1">
Make sure that the view model implements the INotifyPropertyChanged interface and raises change notifications.
The tooltip for the below listbox is set using a setter. Nothing appears for a tooltip on mouse over.
I suspect the issue is the itemssource of the listbox itself. The listbox is bound to a list of AttributeItems called CandidateAttributes. An element of that list is an observablecollection called AttributePath, and the property in the Attribute path I am trying to bind the tooltip to is called ConceptualPath. Below is the definition for CandidateAttributes-
public static List<AttributeItem> CoBRRaAttributes { get; set; }
The AttributeItems class-
public class AttributeItem
{
private string _displayName = "";
private ObservableCollection<CoBRRa_WPF.CoBRRaUtilities.ViewModels.QueryTool.AttributeCollection> _AttributePath;
public AttributeItem(int id, string displayName, ObservableCollection<CoBRRa_WPF.CoBRRaUtilities.ViewModels.QueryTool.AttributeCollection> attributePath)
{
DisplayName = displayName;
AttributePath = attributePath;
}
public ObservableCollection<CoBRRa_WPF.CoBRRaUtilities.ViewModels.QueryTool.AttributeCollection> AttributePath
{
get
{
return _AttributePath;
}
set
{
_AttributePath = value;
}
}
}
The xmal-
<ListBox
Name="lstCandidates"
ItemsSource="{Binding Path=UIProperties.CandidateAttributes}"
>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=DisplayName}">
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Control.ToolTip" Value="{Binding AttributePath.ConceptualPath}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
I can substitute some text in the place of Binding AttributePath.ConceptualPath and the tooltip displays that text. Just can't figure out why it does not work in the binding. How can I get this to work?
You are binding to AttributePath.ConceptualPath but AttributePath returns an ObservableCollection<AttributeCollection> and this one has no ConceptualPath property.
You should either change the type of the AttributePath property to just CoBRRa_WPF.CoBRRaUtilities.ViewModels.QueryTool.AttributeCollection or bind to a specific AttributeCollection, for example the first one:
<Setter Property="Control.ToolTip" Value="{Binding AttributePath[0].ConceptualPath}"/>
Also make sure that ConceptualPath is a public property of the AttributeCollection class.
Edit:
If you want to the display a list of paths in the tooltip, you should use an ItemsControl:
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Control.ToolTip">
<Setter.Value>
<ItemsControl ItemsSource="{Binding AttributePath}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ConceptualPath}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Setter.Value>
</Setter>
</Style>
I'm trying to create a re-usable textblock user control in WPF. The basic idea is as follows:
User does not directly specify the content of the textblock
There are three dependancy properties in my user control called IsToggled, ToggleTrueText, and ToggleFalseText.
The control will display ToggleTrueText if IsToggled is true; or display ToggleFalseText if IsToggled is false.
When IsToggled changes during runtime, the text automatically changes to either ToggleTrueText or ToggleFalseText
I started by adding a PropertyChangedCallback to the IsToggled DP:
Code-behind of the UserControl:
public static readonly DependencyProperty IsToggledProperty =
DependencyProperty.Register("IsToggled", typeof(bool),
typeof(TagToggle), new PropertyMetadata(new
PropertyChangedCallback(OnToggleStateChanged)));
public bool IsToggled
{
get { return (bool)GetValue(IsToggledProperty); }
set { SetValue(IsToggledProperty, value); }
}
//ToggleTrueText and ToggleFalseText are declared similarly to IsToggled
...
private static void OnToggleStateChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
...
}
Xaml of the user control:
<Grid x:Name="LayoutRoot">
<TextBlock x:Name="TheTextBlock" Text="{Binding WhatDoIBindTo}"/>
</Grid>
However, I'm not sure what would be the best way to ensure that TheTextBlock updates its text whenever IsToggled changes during runtime.
Try this:
private static void OnToggleStateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TagToggle ctrl = d as TagToggle;
if (ctrl != null)
{
TheTextBlock.Text = ctrl.IsToggled ? ToggleTrueText. : ToggleFalseText;
}
}
If you want to bind the Text property of the TextBlock you need to make sure that you are binding to properties of the UserControl. You could do this by setting the DataContext property of the TextBlock:
<TextBlock x:Name="TheTextBlock" DataContext="{Binding RelativeSource={RelativeSource AncestorType=UserControl}}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Text" Value="{Binding ToggleTrueText}" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsToggled}" Value="False">
<Setter Property="Text" Value="{Binding ToggleFalseText}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
You can used trigger for this
Please check below code
<TextBlock x:Name="TheTextBlock">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsToggled}" Value="True">
<Setter Property="Text" Value="{Binding ToggleTrueText}"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsToggled}" Value="False">
<Setter Property="Text" Value="{Binding ToggleFalseText}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
How do I switch UserControls based on a property setting in my ViewModel?
If Vm.View = "A"
<Window>
<local:UserControlA/>
</Window>
If Vm.View = "B"
<Window>
<local:UserControlB/>
</Window>
Vm.View is an enum that someday may allow for C, D, and so on. Both UserControls are bound to the same Vm, but they present the data radically different based on the user's input. So a DataTemplate based on type doesn't really work here.
Thoughts?
Add ContentControl inside Window and based on View value you can set it's ContentTemplate using DataTriggers.
<ContentControl Content="{Binding}">
<ContentControl.Style>
<Style TargetType="ContentControl">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<local:UserControlA/>
</DataTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding View}" Value="B">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<local:UserControlB/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
You might leverage DataTemplate's DataType property and let the binding engine take care of the rest...
XAML
<Window.Resources>
<DataTemplate DataType="localEnums:ProduceType.Apples">
<local:ApplesView />
</DataTemplate>
<DataTemplate DataType="localEnums:ProduceType.Oranges">
<local:OrangesView />
</DataTemplate>
</Window.Resources>
<StackPanel>
<ContentPresenter Content="{Binding ProduceType}" />
<Button Content="Change Produce" Click="Button_Click"/>
</StackPanel>
View Model
public class ProduceViewModel : ViewModel
{
public ProduceViewModel()
{
this.ProduceType = ProduceType.Apples;
}
private ProduceType _produceType;
public ProduceType ProduceType
{
get
{
return _produceType;
}
set
{
if (_produceType != value)
{
_produceType = value;
RaisePropertyChanged();
}
}
}
}
Button Click Handler (Violates pure MVVM but just to demonstrate the DataTemplate switching)
private void Button_Click(object sender, RoutedEventArgs e)
{
(this.DataContext as ProduceViewModel).ProduceType = ProduceType.Oranges;
}