Binding gets an old value out of nowhere - c#

I have a control which derives from a TextBox and has 2 additional string properties: Formula and FormulaResult (readonly):
public class SpreadsheetCellControl : TextBox, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = (sender, e) => { };
public string Formula
{
get;
set;
}
public string FormulaResult
{
get
{
if (Formula != null && Formula != "")
{
string formula = Formula;
formula = formula.StartsWith("=") ? formula.Substring(1) : formula;
var calculation = Calculator.Calculate(formula);
return calculation.Result.ToString();
}
else return "";
}
}
protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
Formula = Text;
base.OnPreviewLostKeyboardFocus(e);
}
}
As defined in XAML, Text is bound to Formula when the control is focused and FormulaResult when it is not focused:
<Style TargetType="local:SpreadsheetCellControl" BasedOn="{StaticResource HoverableStyle}">
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Text" Value="{Binding RelativeSource={RelativeSource self}, Path=FormulaResult, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}"/>
<Style.Triggers>
<Trigger Property="IsFocused" Value="true">
<Setter Property="BorderThickness" Value="2" />
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="Text" Value="{Binding Formula, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
</Trigger>
</Style.Triggers>
</Style>
Everything works as it should. For instance, I click the control, type for example "1/2", click anywhere else and it shows "0.5".
Then I set the Formula property from code-behind to "3" (any value actually). Before I click on the control, it shows all three properties correct: 3, 3 and 3. But when I click on the control, the Text property suddenly becomes the old "1/2", which is not stored anywhere. I checked that by placing a TextBlock and binding its Text to all three values of the cell control. Debugging also showed only new values.
P.S. Also "Formula" does not seem to take values by itself from "Text" here: Setter Property="Text" Value="{Binding Formula, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"

The Binding in the Text property Setter inside the Trigger is missing RelativeSource Self:
<Setter Property="Text"
Value="{Binding Path=Formula,
RelativeSource={RelativeSource Self},
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"/>

Related

Can't re-enter into cell edit mode with doubleclick after hitting enter

I have a simple DataGrid with the following column:
<DataGridTextColumn Header="Column" Binding="{Binding ColumnValue, Mode=TwoWay, UpdateSourceTrigger=LostFocus}">
<DataGridTextColumn.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="IsEnabled" Value="{Binding IsColumnEnabled}" />
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="Gray" />
<Setter Property="Foreground" Value="Transparent" />
</Trigger>
</Style.Triggers>
</Style>
</DataGridTextColumn.CellStyle>
</DataGridTextColumn>
Let's assume two rows, the second one is disabled. When I enter a value into the first cell and hit Enter, the focus doesn't jump to the next cell (because it's disabled). The problem is, I cannot enter into edit mode again with doubleclick until the current cell is focused.
Is there any trick to avoid this?
If you want to implement a custom behaviour for Enter key presses, you could override the OnKeyDown method in a custom class:
public class CustomDataGrid : DataGrid
{
protected override void OnKeyDown(KeyEventArgs e)
{
if(e.Key == Key.Return)
{
var currentCell = CurrentCell;
base.OnKeyDown(e);
if (CurrentCell.Column.GetCellContent(CurrentCell.Item)?.Parent is DataGridCell cell && !cell.IsEnabled)
CurrentCell = currentCell;
}
else
{
base.OnKeyDown(e);
}
}
}
Don't forget to change the root element of the DataGrid in your XAML:
<local:CustomerDataGrid ... />

How to Change Button Foreground Color Conditionally in WPF?

I have a button inside ItemsControl (pagination kind control) I have a property named current_page in my view model.
I want to compare current_page value to Content of Button that is (1 / 2 / 3) and want to change the foreground color of a button.
Please have a look at the code.
My ViewModel
public PaginationModel pagination
{
get { return _pagination; }
set { _pagination = value; OnPropertyChanged("pagination"); }
}
My Pagination Model
public class PaginationModel: INotifyPropertyChanged {
private int _total_items;
public int total_items {
get {
return _total_items;
}
set {
_total_items = value;
OnPropertyChanged("total_items");
}
}
private int _items_per_page;
public int items_per_page {
get {
return _items_per_page;
}
set {
_items_per_page = value;
OnPropertyChanged("items_per_page");
}
}
private int _current_page;
public int current_page {
get {
return _current_page;
}
set {
if (value <= total_pages + 1 && value > 0) {
_current_page = value;
OnPropertyChanged("current_page");
}
}
}
private int _total_pages;
public int total_pages {
get {
return _total_pages;
}
set {
_total_pages = value;
OnPropertyChanged("total_pages");
}
}
private ObservableCollection < string > _PageList;
public ObservableCollection < string > PageList {
get {
_PageList = new ObservableCollection < string > ();
for (int i = 0; i < total_pages; i++) {
_PageList.Add((i + 1).ToString());
}
return _PageList;
}
set {
_PageList = value;
OnPropertyChanged("PageList");
}
}
}
My Layout
<ItemsControl ItemsSource="{Binding pagination.PageList}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0">
<Button Content="{Binding}" CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
Command="{Binding DataContext.ChangePage, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"
Width="20"
Margin="10,0"></Button>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Button Style Template
<Style TargetType="{x:Type Button}" >
<Setter Property="Foreground" Value="{StaticResource WordOrangeBrush}" />
<Setter Property="Background" Value="Transparent"></Setter>
<Setter Property="BorderThickness" Value="0"></Setter>
<Setter Property="FontFamily" Value="{StaticResource HelveticaNeue}"></Setter>
<Setter Property="FontSize" Value="14"></Setter>
<Setter Property="Foreground" Value="#ff9f00"></Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}">
<ContentPresenter VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Transparent"></Setter>
<Setter Property="Foreground" Value="#ff9f00" />
</Trigger>
<Trigger Property="IsMouseOver" Value="False">
<Setter Property="Background" Value="Transparent"></Setter>
<Setter Property="Foreground" Value="White" />
</Trigger>
<!--I want to bind current page value in place of 1-->
<DataTrigger Binding="{Binding}" Value="1">
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
A Value Converter Solution
You cannot bind the Value of a DataTrigger to a property, because it is not a dependency property. You can work around this by creating a custom multi-value converter that checks pages for equality.
public class PageEqualityToBooleanConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return (int)values[0] == System.Convert.ToInt32(values[1]);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new InvalidOperationException();
}
}
The first value passed to the converter is the current page as int and the second value is the page on the Button as string. It is a bit odd that your current_page is of type int, but the PageList is a collection of type string, that is why we need to convert the second value.
Create an instance of the converter in your resources before your style, so you can reference it.
<local:PageEqualityToBooleanConverter x:Key="PageEqualityToBooleanConverter"/>
Finally, replace your DataTrigger with the one below. We are using a MultiBinding to be able to bind more than one value. The converter will convert both values to True, if the page values match, otherwise False.
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource PageEqualityToBooleanConverter}">
<Binding Path="DataContext.pagination.current_page" RelativeSource="{RelativeSource AncestorType={x:Type ItemsControl}}"/>
<Binding/>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
The first binding will find the parent ItemsControl to access the view model that contains the current_page. The second binding will bind to the data context of the associated button, which is the page string. If you change the PageList item type to int this will still work.
Recommendations
I want to point out some observations on your code that might help you to improve it.
As I already mentioned, it seems odd that your collection item type differs from your current item property type. Consider making it int or creating a separate page item view model if you plan on having other properties than the page number in it.
You are defining an implicit style for Button, which gets applied to all Button controls in scope. If you use it in your pagination control only this might be ok, but if you intend to use it in other places, the DataTrigger should not be included in it, as it is specific to this data context only. Create separate style based on this one. Consider #BionicCode's comment about this.
As #Andy pointed out in the comments, a ListBox could be a better fit for a paginator, because it has the notion of a SelectedItem (and IsSelected properties on its item containers that can be bound in a DataTrigger), which is what you are trying to do here manually.
The Value property of a DataTrigger cannot be data-bound.
What you should do is to create a type that represents a Page, add a Number and IsCurrentPage property to it and change the type of PageList to be an ObservableCollection<Page> instead of an ObservableCollection<string>.
You could then simply look up the corresponding Page in the PageList and set its IsCurrentPage property from the view model whenever the current_page property is set.
You should also make sure that the getter of the PageList property doesn't create a new collection each time it's invoked.

Wpf Style Trigger triggered only once

I have added a property trigger on a grid as below
<Grid.Style>
<Style TargetType="{x:Type Grid}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="False">
<Setter Property="ToolTip" Value=""></Setter>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="ToolTip" Value="{Binding StoredValue}"></Setter>
</Trigger>
</Style.Triggers>
</Style>
</Grid.Style>
The property is triggered only once when I hover over the grid.
What I require is that the property's (StoredValue) getter has to be called every time when MouseHover happens.
Please help
If you really want to update the tooltip every time it is displayed, you can utilize the ToolTipOpening event to refresh the binding:
<Grid x:Name="grid1" Background="Transparent">
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="ToolTip" Value="{Binding StoredValue,TargetNullValue=''}"/>
<EventSetter Event="ToolTipOpening" Handler="grid1_ToolTipOpening"/>
</Style>
</Grid.Style>
</Grid>
Update the binding in code behind:
private void grid1_ToolTipOpening(object sender, ToolTipEventArgs e)
{
var s = sender as FrameworkElement;
var be = BindingOperations.GetBindingExpressionBase(s, FrameworkElement.ToolTipProperty);
if (be != null)
{
be.UpdateTarget();
}
}
Note: the TargetNullValue='' is necessary in case StoredValue would sometimes return a null. Otherwise the Tooltip wouldn't attempt to open and thus the ToolTipOpening would never happen and the value would never update from the null to a new value.
While I can't explain the nature of problem, here is a quick workaround: rise notification manually, then binding will refresh itself. You trade trigger for event:
<Grid Background="Transparent" MouseEnter="Grid_MouseEnter">
<Grid.Style>
<Style TargetType="{x:Type Grid}">
<!-- normal binding, this line is comment and should be gray -->
<Setter Property="ToolTip" Value="{Binding StoredValue}" />
</Style>
</Grid.Style>
</Grid>
public partial class MainWindow : Window, INotifyPropertyChanged
{
public string StoredValue => "123"; // is called every time mouse is entered
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
// rise notification manually
void Grid_MouseEnter(object sender, MouseEventArgs e) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StoredValue)));
}

How to access usercontrol (toolbar) from ViewModel in MVVM?

how to talk toolbar (it is a user control) on the button to enable a wait cursor.
i have a ViewModel is inherited from viewmodelBase. But i can not use IsWorking on toolbar.
Below code is toolbar's code. i clicked select button. data is selecting from database. Cursor must be turn to wait.after Selecting, Cursor must return normal.
<Button x:Name="Select"
Content="select"
Command="{Binding SelectCommand }">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Cursor" Value="Arrow"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsWorking}" Value="True">
<Setter Property="Cursor" Value="Wait"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
ViewModelBase.cs: there is no inheritance with toolbar. it is a basemodel.
private bool _isWorking = false;
public bool IsWorking
{
get { return _isWorking; }
set
{
_isWorking = value;
OnPropertyChanged("IsWorking");
}
}
Here is the code from the view-model:
public class MainViewModel : ViewModelBase
{
public void Select()
{
IsWorking = true; cursor turn to wait mode
// db Process...
IsWorking = false; cursor turn to hand mode
}
}
How to communicate with toolbar from ViewModel? Click select Cursor must be turn Wait mode. after selection, cursor must be hand(default).
Changing the cursor in WPF sometimes works, sometimes doesn't
From what I see, your problem is that you're trying to bind from your UserControl back to the view/window in which it's located.
The usercontrol, of course, will not be able to bind like this.
You have a few options:
1 . Give the UserControl the View's datacontext:
<local:UserControl1 DataContext="{Binding ElementName=MyWindow}" />
and then in your UserControl you can bind to the ViewModel's IsWorking directly:
<DataTrigger Binding="{Binding IsWorking}" Value="True">
<Setter Property="Cursor" Value="Wait"/>
</DataTrigger>
2 .
Create a Dependency Property on your UserControl and bind to it from the view:
In your usercontrol create a new DP:
public bool MyIsWorking
{
get { return (bool)GetValue(MyIsWorkingProperty ); }
set { SetValue(MyIsWorkingProperty , value); }
}
public static readonly DependencyProperty MyIsWorkingProperty =
DependencyProperty.Register("MyIsWorking", typeof(bool), typeof(UserControl1), new UIPropertyMetadata(false));
In the usercontrol's XAML bind to the DP:
<DataTrigger Binding="{Binding MyIsWorking}" Value="True">
<Setter Property="Cursor" Value="Wait"/>
</DataTrigger>
In your window - bind the DP to the VM's IsWorking property:
<local:UserControl1 MyIsWorking="{Binding IsWorking, ElementName=MyWindow}" />
3 . Finally this will work but it's not recommended!!!**
<DataTrigger Binding="{Binding IsWorking, RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}" Value="True">
<Setter Property="Cursor" Value="Wait"/>
</DataTrigger>
What this does is tries to find the Window in the Visual Tree and use its DataContext. Why isn't it recommended? Because you might not be using this in a Window or you might not want it to be bound to the specific DataContext the containing Window is using. Either way, it IS another possibility.

How can I programatically get keyboard focus on a WPF TreeViewItem?

I'm trying to programatically set keyboard focus to a tree view item (under certain conditions). I've tried 2 methods of setting focus, both of which successfully obtain focus on the TreeViewItem, but lose keyboard focus.
The tree view is bound to a view model:
<TreeView Name="solutionsModel" TreeViewItem.Selected="solutionsModel_Selected"
ItemsSource="{Binding Items, Mode=OneWay}" />
I'm trying to set focus via the TreeViewItem Selected routed event:
private void solutionsModel_Selected(object sender, RoutedEventArgs e)
{
if (solutionsModel.SelectedItem != null && solutionsModel.SelectedItem is SolutionViewModel)
{
if (e.OriginalSource != null && e.OriginalSource is TreeViewItem)
{
FocusManager.SetFocusedElement(solutionsModel, e.OriginalSource as TreeViewItem);
}
}
}
I'm trying to set focus on the TreeViewItem in the ControlTemplate:
<Style d:IsControlPart="True" TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="true">
<Trigger.Setters>
<Setter Property="FocusManager.FocusedElement" Value="{Binding RelativeSource={RelativeSource Self}}"></Setter>
</Trigger.Setters>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="true" />
<Condition Property="IsSelectionActive" Value="false" />
</MultiTrigger.Conditions>
<!--
<MultiTrigger.Setters>
<Setter Property="FocusManager.FocusedElement" Value="{Binding RelativeSource={RelativeSource Self}}"></Setter>
</MultiTrigger.Setters>
-->
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Both of these methods get focus, but lose keyboard focus (TreeViewItem.IsSelectionActive is false). No other element in the window has focus or keyboard focus that I can tell (in a test, I only have one read only textbox on another panel that could get focus). Interestingly, I can get keyboard focus on the (commented out) MultiTrigger where IsSelectionActive is false, but of course that forces keyboard focus on the TreeViewItem at all times.
Is there another way to have a better chance of getting keyboard focus, and what are some conditions where keyboard focus cannot be obtained?
I'd add this as a comment if I could but, why not just have the TreeView handle the focus and work with the item abstractly using the TreeView.SelectedItem. The tree view would always be able to know which item was selected when the typing started. If an item was selected then the TreeView is in focus and you can pipe the keyboard commands through to the item.
There are probably better ways, but I found a way to do this by extending TreeView and TreeViewItem, to have a separate NeedsFocus property to trigger when to set focus.
The tree view:
<local:ModelTreeView x:Name="solutionsModel" ItemsSource="{Binding Items, Mode=OneWay}">
</local:ModelTreeView>
The updated (partial) control template:
<Style d:IsControlPart="True" TargetType="{x:Type local:ModelTreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="NeedsFocus" Value="{Binding NeedsFocus, Mode=TwoWay}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ModelTreeViewItem}">
<ControlTemplate.Triggers>
<Trigger Property="NeedsFocus" Value="true">
<Trigger.Setters>
<Setter Property="FocusManager.FocusedElement" Value="{Binding RelativeSource={RelativeSource Self}}"></Setter>
</Trigger.Setters>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The extended classes:
public class ModelTreeView : TreeView
{
protected override DependencyObject GetContainerForItemOverride()
{
return new ModelTreeViewItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is ModelTreeViewItem;
}
}
public class ModelTreeViewItem : TreeViewItem
{
///--------------------------------------------------------------------------------
/// <summary>This property gets or sets whether the item needs focus.</summary>
///--------------------------------------------------------------------------------
public static readonly DependencyProperty NeedsFocusProperty = DependencyProperty.Register("NeedsFocus", typeof(bool), typeof(ModelTreeViewItem));
public bool NeedsFocus
{
get
{
return (bool)GetValue(NeedsFocusProperty);
}
set
{
SetValue(NeedsFocusProperty, value);
}
}
protected override DependencyObject GetContainerForItemOverride()
{
return new ModelTreeViewItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is ModelTreeViewItem;
}
}
In the view model, NeedsFocus is set to false whenever IsSelected is set.

Categories