Binding Properties of a Converter Inside a Style for CustomControl - c#

I have a converter that maps a percentage into a range. It has two dependency properties for storing the start and end of the range. Below is an example of it's declaration:
<local:PercentToRangeConverter RangeStart="50" RangeEnd="200" />
I'm using the converter in a style for a CustomControl's Width where the MinWidth and the MaxWidth is bound to the RangeStart and the RangeEnd of the converter. Unfortunately, it doesn't work. I've already made sure that the converter itself is working by manually setting the values of the RangeStart and the RangeEnd with an arbitrary number.
I've done two ways for binding the properties of the CustomControl to the converters:
Declare the converter inside the Style.Resources, bind the properties inside there, then use the converter as a static resource.
<Style TargetType="{x:Type local:MyControl}">
<Style.Resources>
<local:PercentToRangeConverter x:Key="PercentToRangeConverter"
RangeStart="{Binding MinWidth, RelativeSource={RelativeSource Self}"
RangeEnd="{Binding MaxWidth, RelativeSource={RelativeSource Self}" />
</Style.Resources>
. . .
Declare the converter inside the converter property of a Binding tag inside the setter's value
. . .
<Setter Property="Width">
<Setter.Value>
<Binding Path="Percent" RelativeSource="{RelativeSource Self}">
<Binding.Converter>
<local:PercentToRangeConverter
RangeStart="{Binding MinWidth, RelativeSource={RelativeSource Self}"
RangeEnd="{Binding MaxWidth, RelativeSource={RelativeSource Self}" />
</Binding.Converter>
</Binding>
</Setter.Value>
</Setter>
. . .
</Style>
Again, these work when I manually set the value. So I think RelativeSource={RelativeSource Self} is incorrect for the binding but I can't think of anything else so that I can bind the CustomControl's Width. I also think that both methods don't have the same proper way for setting the source considering that the way the converter is declared is different for both methods. Either way, I just want to succesfully bind the MinWidth and MaxWidth to the converter.
P.S. Is "declare" even the proper term for that?
EDIT: Here's the code for the Converter as requested.
public class PercentToRangeConverter : DependencyObject, IValueConverter
{
public double RangeStart
{
get => (double)GetValue(RangeStartProperty);
set => SetValue(RangeStartProperty, value);
}
protected static readonly DependencyProperty RangeStartProperty = DependencyProperty.Register("RangeStart", typeof(double), typeof(PercentToRangeConverter), new PropertyMetadata(default(double)));
public double RangeEnd
{
get => (double)GetValue(RangeEndProperty);
set => SetValue(RangeEndProperty, value);
}
protected static readonly DependencyProperty RangeEndProperty = DependencyProperty.Register("RangeEnd", typeof(double), typeof(PercentToRangeConverter), new PropertyMetadata(default(double)));
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (double)value * (RangeEnd - RangeStart) + RangeStart;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
EDIT (1): Forgot to add, I'm actually searching for answers other than MultiBinding. The reason is I'm animating the Percent value. I thought that MultiBinding might affect the performance.

Related

wpf TextBox binding stringformat + converter don't want to work together

I have custom TextBox that only accepts float as input. I bind it to float property (of custom dependency property) using two-way mode. It uses custom converter to do the string conversion to float and vice versa. If i don't format the string, obviously it will display such value such as 3,141592653... which has too many digits behind comma, i want to display 2 decimal places but keep the actual data of the precise value. however, when i tried to run it, string formatting does not work if i set the converter explicitly, unless i remove the converter property it will work. So here i can choose either keep the converter or the string formatting.
I need that converter because wpf default conversion is not enough. It convert empty string as 0 and accept both dot and comma as the delimiter of the decimal. So how do i make my custom textbox to do both stringformat and converter work?
Below is the relevant code of my custom TextBox (called DoubleBox)
My custom TextBox XAML (DoubleBox.XAML)
<TextBox ...
Name="Root">
<TextBox.Text>
<Binding ElementName="Root"
Path="DoubleValue"
Mode="TwoWay"
UpdateSourceTrigger="Explicit"
Converter="{StaticResource DoubleToString}"
StringFormat="{}{0:f2}">
<Binding.ValidationRules>
<validationrules:MyDoubleValidationRule>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
My custom TextBox code-behind (DoubleBox.XAML.cs)
public partial class DoubleBox : TextBox
{
// constructor
public double DoubleValue
{
get { return (double)GetValue(DoubleValueProperty); }
set { SetValue(DoubleValueProperty, value); }
}
public static readonly DependencyProperty DoubleValueProperty =
DependencyProperty.Register(nameof(DoubleValue), typeof(double), typeof(DoubleBox), new PropertyMetadata(0.0));
// method that does the binding update source trigger
// (when focus is lost and enter key is pressed)
}
My double to string converter
public class DoubleToString : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return ((double)value).ToString();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
string str = (string)value;
if (string.IsNullOrEmpty(str))
return 0;
str = str.Replace('.', ',');
if (str.StartsWith(','))
str = "0" + str;
return double.Parse(str);
}
}
Edit for more clarification:
I have tried formatting the double value inside the double to string conversion method. i could round the double first before converting it to string i.e. return Math.Round(((double)value), 2).ToString(); and it display 2 decimal float correctly but remember this is a textbox: the user can modify the value from the textbox itself so it is a TwoWay data binding.
As soon i give my focus to the TextBox it does not display all the floating point precision, it still display 2 decimal float so when i try to lose the focus, the TextBox convert it back to the source property of double as is, which is the formatted value, which is 2 decimal float, so precision is lost so yes this approach does not work for TwoWay binding.
Below is a video link of how it behave in my project if i format the string inside the double to string conversion method:
https://media.giphy.com/media/7wWVlzl3orIeVl3G2B/giphy.gif
i have solved this problem by looking it from other point of view. It exactly does what i want which is display the formatted value (only when focus is lost) and does the conversion
Instead of trying to make both stringformat and converter works, i used triggers that is activated when focus is lost. The trigger sets the Text property to the formatted string so the text display the formatted string only when it does not have focus, but when focus is given, it displays the actual value.
Below is the updated XAML of the DoubleBox.XAML. Actually it is a UserControl now in my code and put the actual TextBox inside the UserControl, no longer modified TextBox but the method still the same by using trigger that display the formatted string when focus is lost
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Text">
<Setter.Value>
<Binding ElementName="Root"
Path="DoubleValue"
Mode="TwoWay"
Converter="{StaticResource DoubleToString}"
UpdateSourceTrigger="Explicit">
<Binding.ValidationRules>
<validationRules:DoubleValidationRule/>
</Binding.ValidationRules>
</Binding>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsFocused" Value="False">
<Setter Property="Text">
<Setter.Value>
<Binding ElementName="Root"
Path="DoubleValue"
Mode="OneWay"
StringFormat="{}{0:f2}">
</Binding>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>

Converter not firing after collection update

I have ran into a issue with the converters... They are not triggering once the bound collection is updated although they trigger when the collection is first populated. I would like to have them fire whenever there is a change in the collection.
So far I have built a simple converter:
public class TableConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
VM.Measurement t = ((VM.Measurement)((TextBlock)value).DataContext);
if (t.Delta != null)
{
if (Math.Abs((double)t.Delta) < t.Tol)
return "Green";
else
return "Red";
}
else
return "Red";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
which is linked to a style
<conv:TableConverter x:Key="styleConvStr"/>
<Style x:Key="CellStyleSelectorTol" TargetType="syncfusion:GridCell">
<Setter Property="Background" Value="{Binding RelativeSource={RelativeSource Self}, Path=Content, Converter={StaticResource styleConvStr}}" />
</Style>
Which is used in this DataGrid
<syncfusion:SfDataGrid x:Name="CheckGrid" BorderBrush="White" Grid.Row="1" Grid.Column="1" AllowEditing="True" ItemsSource="{Binding ChecksList, Mode=TwoWay}" Background="White" SnapsToDevicePixels="False"
ColumnSizer="None" AllowResizingColumns="False" AllowTriStateSorting="True" AllowDraggingColumns="False" CurrentCellEndEdit="CheckGrid_CurrentCellEndEdit" AutoGenerateColumns="False"
NavigationMode="Cell" HeaderRowHeight="30" RowHeight="21" GridPasteOption="None" Margin="20 10 10 10" AllowGrouping="True" SelectedItem="{Binding SelectedLine, Mode=TwoWay}"
SelectionUnit="Row" SelectionMode="Single" RowSelectionBrush="#CBACCB" VirtualizingPanel.IsVirtualizing="True" Visibility="Visible">
<syncfusion:GridTextColumn Width="100" ColumnSizer="SizeToCells" AllowEditing="True" MappingName="Measured" CellStyle="{StaticResource CellStyleSelectorTol}" HeaderText="Measured" TextAlignment="Center" AllowFiltering="False" FilterBehavior="StringTyped"/>
The VM contains an Observable Collection which implements NotifyPropertyChanged all the way down to the Measurement Class. The properties fire up nicely so it is not a binding issue.
private ObservableCollection<Measurement> _checkList = new ObservableCollection<Measurement>();
public ObservableCollection<Measurement> ChecksList
{
get
{
return _checkList;
}
set
{
_checkList = value;
NotifyPropertyChanged();
}
}
Any help with this would be greatly appreciated.
Thanks
EDIT:
Here is the code that updates the collection. Apologies for it being quite messy. Lineitem is the selected line for which Measured and Delta are updated. These are properly displayed in the grid once modified.
public void NewMeasurement(VM.Measurement measurementShell)
{
using (VMEntity DB = new VMEntity())
{
var Check = CheckSets.Where(x => x.ID == SelectedLine.ID).First();
if (Check.Measurement == null)
{
Check.Measurement = measurementShell.Index;
var Lineitem = ChecksList.Where(x => x.ID == SelectedLine.ID).First();
var measurement = DB.Measurements.Where(x => x.Index == Check.Measurement).First();
Lineitem.Measured = (double)measurement.measurement1;
Lineitem.Delta = Lineitem.Measured - Lineitem.Target;
OK, it looks like the problem is that you are changing properties of the cell content item (LineItem, in the NewMeasurement() method), but it's still the same object, so the cell's content doesn't change. The cell's Content is the source for the binding. If that doesn't change, the binding won't wake up and update the target. You're raising PropertyChanged, but this particular binding has no way of knowing you want it to listen to this object for those property changes. Easy enough fix: We'll start telling it exactly what to listen for.
Fortunately the solution means simplifying some of your code. Passing a UI control into a value converter is exotic and not necessary.
What you care about in the converter is Measurement.Delta and Measurement.Tol. When either one changes, the Binding should update its target. You don't want to do that in a clever way. You just want a Binding for each one. That's a Binding's job.
So tell the Binding that you care about those properties, and rewrite the converter to accept both of them as parameters.
<Style x:Key="CellStyleSelectorTol" TargetType="syncfusion:GridCell">
<Setter
Property="Background"
>
<Setter.Value>
<MultiBinding Converter="{StaticResource styleConvStr}">
<Binding Path="Delta" />
<Binding Path="Tol" />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
Converter:
public class TableConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// I'm inferring that Measurement.Delta is Nullable<double>; if that's
// not the case, change accordingly. Is it Object instead?
double? delta = (double?)values[0];
double tol = (double)values[1];
if (delta.HasValue && Math.Abs(delta.Value) < tol)
{
return "Green";
}
return "Red";
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

Aligning my Popup to bottom of Window

im trying to align my Popup to the bottom center of my Window, how ever i'm getting this error:
Additional information: Specified cast is not valid.
This is being caused by my converter double windowWidth = (double)values[0];, how ever the ActualWidth should bee a double! Not too sure on what is going wrong here.
I'm currently showing the data in a MessageBox just to test it at the moment and make sure the values look correct.
Converter
namespace Test_Project.Converters
{
public class NotificationOffsets : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
double windowWidth = (double)values[0];
double notificationWidth = (double)values[1];
MessageBox.Show("Notification Width: " + notificationWidth.ToString() + " Window Width: " + windowWidth.ToString());
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
XAML - Converter Binding
<Style TargetType="Popup" x:Key="PopupNotification">
<Setter Property="IsOpen" Value="True" />
<Setter Property="HorizontalOffset">
<Setter.Value>
<MultiBinding Converter="{StaticResource NotificationOffsets}">
<Binding RelativeSource="{RelativeSource Self}" Path="PlacementTarget.ActualWidth" />
<Binding RelativeSource="{RelativeSource Self}" Path="ActualWidth" />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
Edit:
Breakpoint Data:
Edit 2:
I have now set my PlacementTarget within my Style:
<Setter Property="PlacementTarget" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}" />
Still getting the same error!
You have to return a double on your converter, you are returning a boolean, which cause the invalid cast.
EDIT: You are having Binding problems, you have to set the "PlacementTarget" of your popup in order the get the Width property.
EDIT 2: try this:
<Window x:Class="WpfApplication7.MainWindow"
Name="myWindow"
............
<Setter Property="PlacementTarget" Value="{Binding ElementName=myWindow}"/>

WPF DataGrid Cells Being Randomly Highlighted When Passed Through A Converter

I am attempting to highlight all the cells that start with a given value in a text box that the user enters. I have looked at several examples, generated the following code, that works, sort of, till you start scrolling vertically.
First we have the XAML, which utilizes a multi-binding:
<DataGrid.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Background">
<Setter.Value>
<MultiBinding Converter="{StaticResource CellColor}">
<Binding RelativeSource="{RelativeSource Mode=Self}"/>
<Binding Mode="OneWay" ElementName="contactFilterTextBox" Path="Text"/>
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</DataGrid.CellStyle>
Next the class that I created, which inherits from the IMultiValueConverter class:
public class ColorBasedOnFilterConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
System.Windows.Media.SolidColorBrush cellColor = Brushes.White;
DataGridCell cellToAnalyze = (DataGridCell)values[0];
TextBlock cellContents = (TextBlock)cellToAnalyze.Content;
if (cellContents != null)
{
string cellValue = (string)cellContents.Text;
string filterValue = values[1].ToString();
if (!(String.IsNullOrEmpty(filterValue)))
{
if (cellValue.ToUpper().StartsWith(filterValue.ToUpper()))
{
cellColor = Brushes.LightSalmon;
}
}
}
return cellColor;
}
public object[] ConvertBack(
object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
Initially, it works as I would expect as shown below:
The question then is when I scroll, values that are obviously are selected as shown in the image below?
I am very new to WPF, and surely I am doing something wrong, but I am baffled as to what it is. Any help would be most appreciated.
By default Virtualizing is on for dataGrid which means container i.e. DataGridRow will be generated for only visible items.
Also default value of VirtualizationMode is Recycling which means when you scroll, cells which have moved out of focus will be used to host new visible items that's why you see incorrect item to be coloured because it might have used same row.
To overcome this you can set VirtualizationMode to Standard so that always new rows will be generated to host new visible items.
<DataGrid VirtualizingStackPanel.VirtualizationMode="Standard">
.......
</DataGrid>
In my (admittedly very limited) experience, changing the properties of a DataGridCell in the CodeBehind often goes amiss due to RowVirtualization. I avoid it like the plague when possible. Changing the background color of a cell would be a good example of something that falls into that category.
An alternative (arguably better) option would be to not set the color in the CodeBehind at all, and instead use a DependencyProperty to determine if the cell matches the criteria, and then set the background color (or whatever) using a Trigger. You can get a fuller idea of what I'm talking about from this answer and a little more possibly useful info from this answer to a question I asked recently, especially if you're looking to "find" all of the cells that would be highlighted if they weren't otherwise virtualized. And here's a code excerpt with a very similar goal to the original question to get you started (mostly it's just code adapted from the provided links) in case it helps somebody sometime.
Xaml:
<DataGrid local:DataGridTextSearch.SearchValue="{Binding ElementName=txtSearch, Path=Text, UpdateSourceTrigger=PropertyChanged}">
<DataGrid.Resources>
<local:SearchValueConverter x:Key="SearchValueConverter"/>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="local:DataGridTextSearch.IsTextMatch">
<Setter.Value>
<MultiBinding Converter="{StaticResource SearchValueConverter}">
<Binding RelativeSource="{RelativeSource Self}" Path="Content.Text" />
<Binding RelativeSource="{RelativeSource Self}" Path="(local:DataGridTextSearch.SearchValue)" />
</MultiBinding>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="local:DataGridTextSearch.IsTextMatch" Value="True">
<Setter Property="Background" Value="Orange" />
<Setter Property="Tag" Value="1" />
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
</DataGrid>
The DependencyProperty:
Public NotInheritable Class DataGridTextSearch
Private Sub New()
End Sub
Public Shared ReadOnly SearchValueProperty As DependencyProperty = DependencyProperty.RegisterAttached("SearchValue", GetType(String), GetType(DataGridTextSearch), New FrameworkPropertyMetadata(String.Empty, FrameworkPropertyMetadataOptions.[Inherits]))
Public Shared Function GetSearchValue(obj As DependencyObject) As String
Return DirectCast(obj.GetValue(SearchValueProperty), String)
End Function
Public Shared Sub SetSearchValue(obj As DependencyObject, value As String)
obj.SetValue(SearchValueProperty, value)
End Sub
Public Shared ReadOnly IsTextMatchProperty As DependencyProperty = DependencyProperty.RegisterAttached("IsTextMatch", GetType(Boolean), GetType(DataGridTextSearch), New UIPropertyMetadata(False))
Public Shared Function GetIsTextMatch(obj As DependencyObject) As Boolean
Return CBool(obj.GetValue(IsTextMatchProperty))
End Function
Public Shared Sub SetIsTextMatch(obj As DependencyObject, value As Boolean)
obj.SetValue(IsTextMatchProperty, value)
End Sub
End Class
And the converter (This could definitely stand to be fleshed out, but you get the idea):
Public Class SearchValueConverter
Implements IMultiValueConverter
Public Function Convert(values() As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IMultiValueConverter.Convert
Dim cellText As String = If(values(0) Is Nothing, String.Empty, values(0).ToString())
Dim searchText As String = TryCast(values(1), String)
If Not String.IsNullOrEmpty(searchText) AndAlso Not String.IsNullOrEmpty(cellText) Then
If cellText.ToLower().Contains(searchText.ToLower()) Then
Return True
Else
Return False
End If
End If
Return False
End Function
Public Function ConvertBack(value As Object, targetTypes() As Type, parameter As Object, culture As Globalization.CultureInfo) As Object() Implements IMultiValueConverter.ConvertBack
Return Nothing
End Function
End Class

C# / WPF - DataGrid - Update Element Color after timeout

I currently use the following method to set the colour of my Row Background.
XAML
<Style TargetType="{x:Type xcdg:DataRow}">
<Setter Property="Background">
<Setter.Value>
<MultiBinding Converter="{StaticResource colorConverter}">
<Binding RelativeSource="{RelativeSource Self}" Path="IsSelected"/>
<Binding BindsDirectlyToSource="True" />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
C#
public class ColourConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var isRowSelected = (bool)values[0];
var myInstance = (MyClass) values[1];
// Return default
if (isRowSelected || myInstance == null)
return DependencyProperty.UnsetValue;
// Get the check for the current field
return GetColour(myInstance) ?? DependencyProperty.UnsetValue;
}
private static SolidColorBrush GetColour(MyClass myInstance)
{
if (heartbeat == null)
{
return null;
}
// Is it more two minutes old?
return (myInstance.CreatedDateTime + TimeSpan.FromMinutes(2) < Clock.UtcNow())
? Brushes.LightPink
: Brushes.LightGreen;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException(this.GetType().Name + " cannot convert back");
}
}
The problem is that this Converter is only called on population of the DataRow with new values. What I really need is some sort of callback to change the colour after a certain time or to have the Converter reevaluated periodically.
The colour update doesn't have to be instant, just within a few seconds. If I have a callback for each row then I'd need as many threads to match (They are created and hence expire (which changes their colour) at different times), with ~1000 rows this doesn't seem like an efficient option.
The other option is to poll the rows on one thread periodically and reevaluate the converter on each iteration (Every 5 seconds?). I think this is likely the way to go but I don't know how to go about it in WPF.
Perhaps there's another approach or built in support for such a task?
Thanks in advance!
Should be possible to get the BindingExpression from the DataRow and simply call UpdateSource/UpdateTarget manually as many times as you need...
BindingExpression binding = Control.GetBindingExpression(DataRow.BackgroundProperty)
binding.UpdateSource;
Don't forget to also change the UpdateSourceTrigger property on the binding.

Categories

Resources