WPF XAML datatrigger fails to fire after programmatically changing property value C# - c#

I have this code in my xaml which says to color my button when I hover my mouse and click my mouse over the button.
<Border x:Class="DatasetGrid.RowHeaderButton"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300" MinWidth="30" Width="Auto">
<Border.Resources>
<SolidColorBrush x:Key="ButtOverBrush" Color="#53C3D5" Opacity="0.2"></SolidColorBrush>
<SolidColorBrush x:Key="ButtPressedBrush" Color="#53C3D5" Opacity="0.5"></SolidColorBrush>
</Border.Resources>
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="Transparent"></Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource ButtOverBrush}"></Setter>
</Trigger>
<DataTrigger Binding="{Binding IsMouseDown, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="Background" Value="{StaticResource ButtPressedBrush}"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
This works all well and good, but I find that as soon as I change the Background color in code behind, the above MouseOver and MouseDown triggers don't fire anymore.
RowHeaderButton rhb = RowHeadersColumn.VisibleRowHeaders[cell.CellInfo.RowIndex];
rhb.Background = new SolidColorBrush(Color.FromArgb(100, 83, 195, 213));
I'm quite new to WPF so I'm not sure what's going wrong.
Edit:
So to give some more information, my control above is a RowHeaderButton, i.e the row header to a grid. Each row in the grid has it's own row header button. So when the user hovers over or clicks it, it should change from white to the specified SolidColorBrush above.
In the code behind of another control, DataGrid.xaml.cs, I have the below code (simplified) which will change the color of the row header when when a cell in the same row of the grid is selected or not.
void UpdateSelectedCells() {
foreach (Cell cell in VisibleColumns.SelectMany(c => c.VisibleCells))
{
int cellRowIndex = cell.CellInfo.RowIndex;
cell.IsSelected = SelectedCells.Contains(cell.CellInfo);
foreach (RowHeaderButton rhb in RowHeadersColumn.VisibleRowHeaders)
{
int rowHeaderIndex = Convert.ToInt16(rhb._default.Text) - 1;
if (cellRowIndex == rowHeaderIndex)
{
if (cell.IsSelected)
{
rhb.Background = new SolidColorBrush(Color.FromArgb(100, 83, 195, 213));
}
else
{
bool rowselected = false;
//need to check if any other cell in the row is selected, if not then color row header white
foreach (CellInfo celll in SelectedCells)
{
if (celll.RowIndex == cellRowIndex)
{
rowselected = true;
break;
}
}
if (rowselected == false)
rhb.Background = Brushes.White;
}
}
}
}
}
I don't have a ViewModel for this.

The triggers are firing, but their setters are being overridden.
This is due to Dependency Property Value Precendence. If the Background property is set programmatically or as an attribute in the XAML, that value will override anything value any style setter gives it. In general, this is desirable behavior: You want to be able to override what the style does on an individual control.
The solution to this is to do all of your background brush changes in style triggers. Your code behind must have some reason for setting the background brush when it does. Whatever that is, find a way to do it with a trigger. Set a property on the viewmodel and write a trigger on that property.
If you need help translating that high level abstraction into your own code, please share enough code for me to understand why and where the codebehind is setting the Background, and what (if anything) you have for a viewmodel.

I solved the issue by creating a new Dependancy Property and binding it to a data trigger.
public bool IsCellSelected
{
get { return (bool)GetValue(IsCellSelectedProperty); }
set { SetValue(IsCellSelectedProperty, value); }
}
public static readonly DependencyProperty IsCellSelectedProperty =
DependencyProperty.Register("IsCellSelected", typeof(bool), typeof(RowHeaderButton), new PropertyMetadata(null));
In my xaml I have:
<DataTrigger Binding="{Binding IsCellSelected, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="Background" Value="{StaticResource ButtPressedBrush}"></Setter>
</DataTrigger>
And in my code behind I set the value using:
RowHeaderButton rhb = RowHeadersColumn.VisibleRowHeaders[cell.CellInfo.RowIndex];
rhb.IsCellSelected = true; //or false
Now my button hover and button click events are not overridden.

Related

Getting binding expressions from code behind from Datatemplate Triggers

I've been set to maintain a wpf application where there is a listbox for logging purposes.
The items displayed using listbox are of type TextMessage, i.e. the listbox is bound to these text messages via
ObservableCollection<TextMessage> Messages;
listBox.DataContext = Messages;
Messages are then added with something like
Messages.Add(new TextMessage("Test", TypeOfMessage.Headline));
This is the definition of the class TextMessage
public enum TypeOfMessage
{
Normal,
Headline,
Focus,
Important,
Fail,
Success
}
public class TextMessage
{
public TextMessage(string content, TypeOfMessage typeOfMessage)
{
Content = content;
TypeOfMessage = typeOfMessage;
CreationTime = DateTime.Now;
}
public string Content { get; }
public TypeOfMessage TypeOfMessage { get; }
public DateTime CreationTime { get; }
}
The xaml definition for the listbox is something like this:
<ListBox x:Name="listBox" HorizontalAlignment="Left" Height="196" Margin="101,77,0,0" VerticalAlignment="Top" Width="256" ItemsSource="{Binding}" SelectionMode="Multiple">
<ListBox.InputBindings>
<KeyBinding
Key="C"
Modifiers="Control"
Command="Copy"
/>
</ListBox.InputBindings>
<ListBox.CommandBindings>
<CommandBinding
Command="Copy"
Executed="DoPerformCopy"
/>
</ListBox.CommandBindings>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock x:Name="TextToShow" Text="{Binding Content}"></TextBlock>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding TypeOfMessage}" Value="Normal">
<Setter TargetName="TextToShow" Property="Foreground" Value="Black"/>
</DataTrigger>
<DataTrigger Binding="{Binding TypeOfMessage}" Value="Focus">
<Setter TargetName="TextToShow" Property="Foreground" Value="Black"/>
<Setter TargetName="TextToShow" Property="FontWeight" Value="Bold"/>
</DataTrigger>
<DataTrigger Binding="{Binding TypeOfMessage}" Value="Headline">
<Setter TargetName="TextToShow" Property="Foreground" Value="RoyalBlue"/>
<Setter TargetName="TextToShow" Property="FontWeight" Value="Bold"/>
</DataTrigger>
<DataTrigger Binding="{Binding TypeOfMessage}" Value="Important">
<Setter TargetName="TextToShow" Property="Foreground" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding TypeOfMessage}" Value="Fail">
<Setter TargetName="TextToShow" Property="Foreground" Value="Red"/>
<Setter TargetName="TextToShow" Property="FontWeight" Value="Bold"/>
</DataTrigger>
<DataTrigger Binding="{Binding TypeOfMessage}" Value="Success">
<Setter TargetName="TextToShow" Property="Foreground" Value="Green"/>
<Setter TargetName="TextToShow" Property="FontWeight" Value="Bold"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This works nicely (i.e messages are displayed in the listbox in different font weight and color depending on their type), but now for the question :
Is there any way using BindingExpression or any other means to get the font formatting and coloring from code behind from the xaml definitions ?
The reason is that I want to just have the formatting in one place (just in the xaml as it is right now) but still be able to reuse it when I want to copy the contents (using code behind) including font formatting to the clipboard.
Example:
private void DoPerformCopy()
{
RichTextBox rtb = new RichTextBox();
foreach (TextMessage message in (listBox as ListBox)?.SelectedItems.Cast<TextMessage>().ToList())
{
TextPointer startPos = rtb.CaretPosition;
rtb.AppendText(message.Content);
rtb.Selection.Select(startPos, rtb.CaretPosition.DocumentEnd);
//
// Here it would be very nice to instead having multiple switch statements to get the formatting for the
// TypeOfMessage from the xaml file.
SolidColorBrush scb = new SolidColorBrush(message.TypeOfMessage == TypeOfMessage.Fail ? Colors.Red);
//
rtb.Selection.ApplyPropertyValue(RichTextBox.ForegroundProperty, scb);
}
// Now copy the whole thing to the Clipboard
rtb.Selection.Select(rtb.Document.ContentStart, rtb.Document.ContentEnd);
rtb.Copy();
}
Since I'm new to wpf, I'd really appreciate if someone has a tip for solving this. (I've tried hard to find an solution here at stackoverflow, but so far I've been unsuccessful)
Thanks in advance,
King regards
Magnus
Make a ContentPresenter with Content set to your TextMessage. Set the ContentTemplate to listBox.ItemTemplate and apply the template. It will create the visuals (TextBlock in this case). Then, just parse off the values from the TextBlock.
Also, your RichTextBox selection code wasn't working quite right so I fixed that by just inserting TextRanges to the end of it instead of trying to get the selection right.
private void DoPerformCopy(object sender, EventArgs e)
{
RichTextBox rtb = new RichTextBox();
foreach (TextMessage message in (listBox as ListBox)?.SelectedItems.Cast<TextMessage>().ToList())
{
ContentPresenter cp = new ContentPresenter();
cp.Content = message;
cp.ContentTemplate = listBox.ItemTemplate;
cp.ApplyTemplate();
var tb = VisualTreeHelper.GetChild(cp, 0) as TextBlock;
var fg = tb.Foreground;
var fw = tb.FontWeight;
var tr = new TextRange(rtb.Document.ContentEnd, rtb.Document.ContentEnd);
tr.Text = message.Content;
tr.ApplyPropertyValue(RichTextBox.ForegroundProperty, fg);
tr.ApplyPropertyValue(RichTextBox.FontWeightProperty, fw);
}
// Now copy the whole thing to the Clipboard
rtb.Selection.Select(rtb.Document.ContentStart, rtb.Document.ContentEnd);
rtb.Copy();
}

WPF Datagrid DataBinding DataView

Pre-Warning sorry WPF new guy here:
I have a DataGrid bound to a DataTable's DefaultView
ResultDataGrid.ItemsSource = resultTable.DefaultView;
I know the column names, and I need to change a column's foreground if another column is 1 (always 0 or 1)
Currently what I have:
private void ResultDataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.Column.Header.ToString() == "columnName")
{
e.Column.CellStyle = FindResource("columnStyle") as Style;
}
}
and in XAML:
<Window.Resources>
<Style TargetType="DataGridCell" x:Key="columnStyle">
<Setter Property="Foreground" Value="Black"/>
<Style.Triggers>
<DataTrigger Binding="{Binding resultTable, Path={StaticResource otherColumnName}}" Value="1">
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
Where otherColumnName is set in the constructor
public ResultsCustom(DataTable resultclass, CustomQuery query)
{
// Some other stuff
this.Resources.Add("otherColumnName", COLUMN_NAME);
}
The XAML Style seems to not have the correct path, any help would be appreciated!
I'm not sure about this line of code:
Path={StaticResource otherColumnName}
Are you attempting to compare the property "otherColumnName" of the resultTable to see if this is 1? This wouldn't be a static resource, and you could change your binding to:
Path=otherColumnName

Deselect selected item in wpf tree view (MVVM)

I am using a WPF treeview, when i click on a node\item once it gets selected. When the user clicks on the selected node the second time i want this node\item to get deselected i.e. i should be able to get the event. IsSelected is not called if i click on the selected node\item that is already selected. How do i get it to work?
<TreeView Grid.Column="0" Grid.Row="1" ItemsSource="{Binding source}" Name="mytreeview">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding displaytext}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
and in my view model i have
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
if (value != _isSelected)
{
_isSelected = value;
if (_isSelected)
{
//my logic
}
this.OnPropertyChanged("IsSelected");
}
}
}
if (value != _isSelected)
Assuming that the UI is even trying to set something, that line is blocking your toggle logic. Something like this should fix at least that part.
set
{
if (value != _isSelected)
{
_isSelected = value;
this.OnPropertyChanged("IsSelected");
}
else if(_isSelected)
{
IsSelected = false;
}
}
Otherwise the UI is checking the selection before setting the value and you'll need to handle it through some other user interaction like handling deselection on click.
I know this is a bit late but I've recently had the same requirement (i.e. unselecting a selected TreeViewItem on the second click) and I solved it by declaring an event handler for the 'MouseLeftButtonUp' event in a 'Style' entry for the ItemContainerStyle of the TreeView as follows:
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="MouseLeftButtonUp" Handler="TreeViewItem_MouseLeftButtonUp"/>
</Style>
</TreeView.ItemContainerStyle>
The event handler in the code behind was as follows:
private TreeViewItem prevTVI;
private void TreeViewItem_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
TreeViewItem tvi = (TreeViewItem)sender;
if (tvi == this.prevTVI)
{
this.prevTVI = null;
if (tvi.IsSelected)
tvi.IsSelected = false;
}
else
this.prevTVI = tvi;
e.Handled = true;
}
Now, I would like to ask if anyone thinks this approach breaks the MVVM pattern? I personally don't think so as the event handler is only concerned with the View and its objects not anything else but I would like to hear what others have to say, especially if someone has an alternative.
The IsSelected property is only changed when you select a new item. Clicking on the same item twice will normally have no effect. You would need to register the MouseDown event on the TreeView, and then force the item to be deselected in the code-behind.

Styles and bindings acting oddly

Right. I've got a small program (that replicates my issue). Basically, it tries to bind to some properties of the object it's styling. It kind of works: it gives me the default value (from the dependency property). I've been thinking this may be because the Style's RelativeSource Self isn't the same as the TextBox it's styling's one. But I don't know. I've tried debugging this, checking time and again that the value set in XAML was actually set. The thing is, with a smaller test program it works. This is just a scale up from that. I don't know what's going wrong.
Thanks!
The code for reproducing this issue:
MainWindow.xaml
<Window x:Class="MyNamespace.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lcl="clr-namespace:MyNamespace"
Title="My title." Height="350" Width="425" MaxHeight="350" MaxWidth="425" MinHeight="350" MinWidth="425">
<Window.Resources>
<ResourceDictionary Source="TestDictionary.xaml"/>
</Window.Resources>
<Grid>
<TextBox Style="{StaticResource TextBoxWithDefault}" FontSize="36" lcl:MyOptions.Default="Not default." VerticalAlignment="Center"/>
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
namespace MyNamespace
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
public static class MyOptions
{
public static string GetDefault(DependencyObject obj)
{
return (string)obj.GetValue(DefaultProperty);
}
public static void SetDefault(DependencyObject obj, string value)
{
obj.SetValue(DefaultProperty, value);
}
public static readonly DependencyProperty DefaultProperty =
DependencyProperty.RegisterAttached(
"Default",
typeof(string),
typeof(MyOptions),
new PropertyMetadata("Default"));
}
}
TestDictionary.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lcl="clr-namespace:MyNamespace"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<Style TargetType="TextBox" x:Key="TextBoxWithDefault">
<Style.Resources>
<Label Content="{Binding Path=(lcl:MyOptions.Default), Mode=TwoWay, RelativeSource={RelativeSource Self}}"
Foreground="LightGray"
FontSize="{Binding Path=(FontSize), Mode=TwoWay, RelativeSource={RelativeSource Self}}" x:Key="TheLabel"/>
</Style.Resources>
<Style.Triggers>
<Trigger Property="Text" Value="{x:Static sys:String.Empty}">
<Setter Property="Background">
<Setter.Value>
<VisualBrush AlignmentX="Left" AlignmentY="Center" Stretch="None" Visual="{DynamicResource TheLabel}"/>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="Text" Value="{x:Null}">
<Setter Property="Background">
<Setter.Value>
<VisualBrush AlignmentX="Left" AlignmentY="Center" Stretch="None" Visual="{DynamicResource TheLabel}"/>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter Property="Background" Value="White"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
I have no idea what's going wrong here, as a scaled down version of this works perfectly. There's probably something I overlooked, that will seem pretty obvious when I find it. But I can't find it now.
EDIT: Well, it seems I was dumb. The original version (here) uses a Trigger, which means that it gets the parent textbox's value. The question now is: how can I get it working?
Thanks for your time!
The real show-stopper here is that when you use the Label in a VisualBrush, the label isn't part of the TextBox' "Visual Tree" (see for example Sheldon Xiao's answer to this similar question on MSDN: Binding Problem inside VisualBrush).
This means that the label won't inherit the text box' DataContext, and you can't reach the text box from a RelativeSource binding either. In contrast, the accepted answer in your other post sets the actual content of a button, which does make the content part of the button's visual tree.
So I don't think there's a pure XAML solution to this problem - pushing the correct MyOptions.Default from the text box to the label. One possible code-based solution is to scrap the TextBoxWithDefault style and do everything from your attached property when Default changes:
...
public static readonly DependencyProperty DefaultProperty =
DependencyProperty.RegisterAttached(
"Default",
typeof(string),
typeof(MyOptions),
//Listen for changes in "Default":
new PropertyMetadata(null, OnMyDefaultChanged));
private static void OnMyDefaultChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
var text = (TextBox)sender;
var myDefault = e.NewValue;
var defaultLabel = new Label();
defaultLabel.Foreground = Brushes.LightGray;
//Explicitly bind the needed value from the TextBox:
defaultLabel.SetBinding(Label.ContentProperty,
new Binding()
{
Source = text,
Path = new PropertyPath(MyOptions.DefaultProperty)
});
text.Background = new VisualBrush()
{
Visual = defaultLabel,
AlignmentX = AlignmentX.Left,
AlignmentY = AlignmentY.Center,
Stretch = Stretch.None
};
text.TextChanged += new TextChangedEventHandler(OnTextWithDefaultChanged);
}
private static void OnTextWithDefaultChanged(object sender,
TextChangedEventArgs e)
{
var text = (TextBox)sender;
var defaultLabel = (text.Background as VisualBrush).Visual as Label;
defaultLabel.Visibility = string.IsNullOrEmpty(text.Text) ?
Visibility.Visible :
Visibility.Collapsed;
}

WPF Hide TextBox Text on Focus

I have a textbox containing a decimal value on my interface that I want to clear whenever the user selects it.
However, if the user doesn't make any changes and selects another interface element I need the text to revert to whatever it was previous to the clear.
So far I have the the following style:
<Style x:Key="CustomTextBoxStyle" TargetType="{x:Type TextBox}">
<Setter Property="Text" Value="{Binding RelativeSource={RelativeSource Self}, Path=Tag}"/>
<Style.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter Property="Text" Value="{x:Null}" />
</Trigger>
</Style.Triggers>
</Style>
And then the following to use the style:
<TextBox Style="{DynamicResource CustomTextBoxStyle}"
Tag="{Binding myDecimalValue, StringFormat=#.###}"
TabIndex="1" />
However, in this scenario the value reverts back to what it was even when the user enters a new value.
Can anyone tell me the best way to go about achieving this?
Thanks,
The solution here is not to hide the text but to store it in a variable for use later. In C# the code would be something like:
string _originalValue;
public OnFocus(){
_originalValue = TextBox.Text;
TextBox.Text = "";
}
public LostFocus(){
if(TextBox.Text == "")
TextBox.Text = _originalValue;
}
You could set the forground colour to transparent to hide the text if that's appropriate.
If you actually want to delete the text you should do what Ryan Amies is suggesting on the viewmodel which you should be able to get through the datacontext.
Thanks for the help, but I was able to achieve what I was looking for and adhere to MVVM principles by using the AttachedProperty described at the following:
https://stackoverflow.com/a/7972361/1466960
This allowed me to bind the IsFocused property to a value in my view model and proceed in a similar fashion to the one described by Ryan Amies.
View Model:
bool isFocused = false;
double original;
public bool IsFocused
{
get
{
return isFocused;
}
set
{
isFocused = value;
if (isFocused)
{
original = current;
current = "";
}
else
{
if (HundredPercentLine == "")
current = original;
}
OnPropertyChanged(new PropertyChangedEventArgs("IsFocused"));
}
}

Categories

Resources