WPF binding IValueConverter and the width of another control - wpf

The following code worked fine. There was an error elsewhere in the code. Still, the advice given is good.
I am trying to bind the Width of a TextBox to a percentage of the Width of a parent control. I know I can accomplish something similar by simply setting a Margin, but I was wondering why this doesn't work.
First, I set a reference to an IValueConverter in the resources collection of my user control:
<UserControl.Resources>
<local:TextBoxWidthConverter x:Key="txtWidthConv" />
</UserControl.Resources>
In the main xaml, I have the following:
<StackPanel Name="parentPanel" Width="300">
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Hidden" Name="scroller" Width="{Binding Width,
ElementName=parentPanel, Converter={StaticResource txtWidthConv}}">
<StackPanel Orientation="Horizontal">
<TextBox></TextBox>
</StackPanel>
</ScrollViewer>
</StackPanel>
The ivalueconverter looks like this:
public class TextBoxWidthConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
double result = (double)value;
if (!Double.IsNaN(result))
{
result = result * .25;
}
else
{
result = 100D;
}
return result;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new System.NotImplementedException("Not implemented.");
}
#endregion
}
Setting the width property does nothing here, let alone setting the IValueConverter. I would expect the ScrollViewer to be 1/4 the width of the parent StackPanel.

Set the ScrollViewer's HorizontalAlignment to something other than Stretch.
Also, you should bind to the ActualWidth property.

Let the layout system work for you instead of fighting it. Grid will automatically handle relative sizing:
<StackPanel Name="parentPanel" Width="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="3*" />
</Grid.ColumnDefinitions>
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Hidden" Name="scroller">
<StackPanel Orientation="Horizontal">
<TextBox></TextBox>
</StackPanel>
</ScrollViewer>
</Grid>
</StackPanel>

Related

WPF MarkupExtension binding error

refers to implementing a multilanguage interface now I have a problem. I have a grid where its ItemSource it's a Dictionary<string, string>: I can't give the key value as parameter of the markup extension because I have the following exception
A 'Binding' cannot be set on the 'Value1' property of type 'TranslateMarkupExtension'.
A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
This is the xaml where I use the markup extension:
<ItemsControl ItemsSource="{Binding MyDictionary}" Grid.Row="0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Label Content="{TranslateMarkup Value1 = {Binding Path = Key}}" Grid.Column="0" />
<TextBox Tag="{Binding Key, Mode=OneWay}" Text="{Binding Value, Mode=OneWay}" Width="200" Height="46" VerticalContentAlignment="Center" Grid.Column="1" LostFocus="TextBox_OnLostFocus" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I can't find any workaround..I need the dictionary as item source because the label will show the key and the textbox the corresponding value..I tried also replacing the dictionary with a list of object but the problem still remains.
Does anyone have a good hint? Thanks in advance.
As suggested by Ed Plunkett, the solution is the ValueConverter.
So the label is know this:
<Label Content="{Binding Key, Converter={StaticResource MultiLangConverter}}" Grid.Column="0" />
where my ValueConverter is the following:
public class StringToMultiLangConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var stringValue = (string) value;
if (string.IsNullOrEmpty(stringValue))
return string.Empty;
string multiLang = LangTranslator.GetString(stringValue);
return multiLang ?? value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return DependencyProperty.UnsetValue;
}
}
Thanks for the help!

Texttrimming in Auto grid column

I want something very simple in WPF, but I don't get it to work:
I have a grid with 2 columns: one * and one Auto. The second column contains a TextBlock. I need texttrimming to work with this TextBlock. This doesn't work currently, because the TextBlock goes outside the bounds of the grid.
Extra info:
The second column should be juste wide enough to contain the TextBlock. The first column should contain all remaining space. If the Grid isn't wide enough to contain the desired width of the TextBlock, the text should be trimmed.
The width of the Grid changes when resizing the window.
Nothing is static (not the text, no sizes), so hardcoded values can not be used.
ClipToBounds property doesn't fix this issue.
I can't bind MaxWidth of the TextBlock to the width of the column, otherwise the TextBlock will only getting smaller, but never bigger when resizing the window.
Code to reproduce the issue (for example in Kaxaml):
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DockPanel>
<Grid Height="20" Background="Blue" DockPanel.Dock="Top" Margin="100 0 100 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto" MaxWidth="200"/>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="1"
Background="Red"
Text="Test tralalalalalalalalalala long string this should be trimmed!"
TextTrimming="CharacterEllipsis"/>
</Grid>
</DockPanel>
</Page>
Any suggestions?
Second solution:
Use a Converter like this:
namespace StackStuff{
class WidthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if(value is Double)
{
return (double)value - 200; // 200 = 100+100 form the grid margin
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
In the View, you will have:
xmlns:local="clr-namespace:StackStuff"
Then, you have to add the converter for it to be used:
<Window.Resources>
<local:WidthConverter x:Key="WidthConverter"/>
</Window.Resources>
And then you have to implement the converter:
<DockPanel Background="Green" x:Name="dock">
<Grid Height="20" Background="Blue" DockPanel.Dock="Top" Margin="100 0 100 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock MaxWidth="{Binding ActualWidth, Converter={StaticResource WidthConverter}, ElementName=dock}"
Grid.Column="1"
Hope this is what you wanted.

Placing a PopUp to the top of the mouse-cursor position

I am working on an interactive chart, where I am displaying a popup with more information, when the user clicks on a data-point.This works fine so far and this is the popup definition:
<Popup IsOpen="{Binding PopupViewModel.IsOpen}"
Placement="Mouse"
HorizontalOffset="-150">
<Popup.Resources>
<DataTemplate DataType="{x:Type ViewModels:DataPointPopUpContentViewModel}">
<Views:DataPointPopUpContentView/>
</DataTemplate>
</Popup.Resources>
<Border BorderThickness="1" BorderBrush="Black" Background="White">
<ContentPresenter Content="{Binding PopupViewModel}" />
</Border>
</Popup>
The default placement of the popup, when using Placement="Mouse" is at the bottom right of the mouse-cursor. However I want the popup to be placed just at the top edge the mouse-cursor. As you can see I have achieved the horizontal centering by setting HorizontalOffset="-150", which is have of the fixed popup-width (Width=300). For the vertical placement I have the problem, that the popup-height is not fixed, since I am displaying an image of variable size and aspect-ratio inside it. I have therefore tried to set VerticalOffset to the ActualHeight of the pupup by adding VerticalOffset="{Binding ActualHeight}". This does not work unfortunately. Any ideas on what I am doing wrong and how to achieve my goal?
First of all you need a converter:
public class MultiplyConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is double && parameter is double)
{
return ((double)value) * ((double)parameter);
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
Then try to bind the VerticalOffset property to the ActualHeight of the Popup's child:
<Window.Resources>
<local:MultiplyConverter x:Key="MultiplyConverter" />
<sys:Double x:Key="Factor">-.5</sys:Double>
</Window.Resources>
<Popup IsOpen="{Binding PopupViewModel.IsOpen}"
Placement="Mouse"
HorizontalOffset="-150">
<Popup.VerticalOffset>
<Binding Path="Child.ActualHeight" RelativeSource="{RelativeSource Self}"
Converter="{StaticResource MultiplyConverter}" ConverterParameter="{StaticResource Factor}" />
</Popup.VerticalOffset>
<!-- popup content -->
</Popup>
I hope it can help you.

Show the validation error template on a different control in WPF

I have a UserControl that contains other controls and a TextBox. It has a Value property that is bound to the TextBox text and has ValidatesOnDataErrors set to True.
When a validation error occurs in the Value property binding, the error template (standard red border) is shown around the entire UserControl.
Is there a way to show it around the TextBox only?
I'd like to be able to use any error template so simply putting border around textbox and binding its color or something to Validation.HasError is not an option.
Here's my code:
<DataTemplate x:Key="TextFieldDataTemplate">
<c:TextField DisplayName="{Binding Name}" Value="{Binding Value, Mode=TwoWay, ValidatesOnDataErrors=True}"/>
</DataTemplate>
<controls:FieldBase x:Name="root">
<DockPanel DataContext="{Binding ElementName=root}">
<TextBlock Text="{Binding DisplayName}"/>
<TextBox x:Name="txtBox"
Text="{Binding Value, Mode=TwoWay, ValidatesOnDataErrors=True}"
IsReadOnly="{Binding IsReadOnly}"/>
</DockPanel>
UserControl (FieldBase) is than bound to ModelView which performs validation.
to accomplish this task I've used this solution. It uses converter, that "hides" border by converting (Validation.Errors).CurrentItem to Thickness.
<Grid>
<Grid.Resources>
<data:ValidationBorderConverter
x:Key="ValidationBorderConverter" />
</Grid.Resources>
<Border
BorderBrush="#ff0000"
BorderThickness="{Binding
ElementName=myControl,
Path=(Validation.Errors).CurrentItem,
onverter={StaticResource ValidationBorderConverter}}">
<TextBox
ToolTip="{Binding
ElementName=myControl,
Path=(Validation.Errors).CurrentItem.ErrorContent}" />
</Border>
</Grid>
ValidationBorderConverter class is pretty simple:
[ValueConversion(typeof(object), typeof(ValidationError))]
public sealed class ValidationBorderConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
return (value == null) ? new Thickness(0) : new Thickness(1);
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}

How do I make a WPF data template fill the entire width of the listbox?

I have a ListBox DataTemplate in WPF. I want one item to be tight against the left side of the ListBox and another item to be tight against the right side, but I can't figure out how to do this.
So far I have a Grid with three columns, the left and right ones have content and the center is a placeholder with it's width set to "*". Where am I going wrong?
Here is the code:
<DataTemplate x:Key="SmallCustomerListItem">
<Grid HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="*"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<WrapPanel HorizontalAlignment="Stretch" Margin="0">
<!--Some content here-->
<TextBlock Text="{Binding Path=LastName}" TextWrapping="Wrap" FontSize="24"/>
<TextBlock Text=", " TextWrapping="Wrap" FontSize="24"/>
<TextBlock Text="{Binding Path=FirstName}" TextWrapping="Wrap" FontSize="24"/>
</WrapPanel>
<ListBox ItemsSource="{Binding Path=PhoneNumbers}" Grid.Column="2" d:DesignWidth="100" d:DesignHeight="50"
Margin="8,0" Background="Transparent" BorderBrush="Transparent" IsHitTestVisible="False" HorizontalAlignment="Stretch"/>
</Grid>
</DataTemplate>
I also had to set:
HorizontalContentAlignment="Stretch"
on the containing ListBox.
<Grid.Width>
<Binding Path="ActualWidth"
RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type ScrollContentPresenter}}" />
</Grid.Width>
Ok, here's what you have:
Column 0: WrapPanel
Column 1: Nothing
Column 2: ListBox
It sounds like you want WrapPanel on the left edge, ListBox on the right edge, and space to take up what's left in the middle.
Easiest way to do this is actually to use a DockPanel, not a Grid.
<DockPanel>
<WrapPanel DockPanel.Dock="Left"></WrapPanel>
<ListBox DockPanel.Dock="Right"></ListBox>
</DockPanel>
This should leave empty space between the WrapPanel and the ListBox.
Extending Taeke's answer, setting the ScrollViewer.HorizontalScrollBarVisibility="Hidden" for a ListBox allows the child control to take the parent's width and not have the scroll bar show up.
<ListBox Width="100" ScrollViewer.HorizontalScrollBarVisibility="Hidden">
<Label Content="{Binding Path=., Mode=OneWay}" HorizontalContentAlignment="Stretch" Height="30" Margin="-4,0,0,0" BorderThickness="0.5" BorderBrush="Black" FontFamily="Calibri" >
<Label.Width>
<Binding Path="Width" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBox}}" />
</Label.Width>
</Label>
</ListBox >
The Grid should by default take up the whole width of the ListBox because the default ItemsPanel for it is a VirtualizingStackPanel. I'm assuming that you have not changed ListBox.ItemsPanel.
Perhaps if you got rid of the middle ColumnDefinition (the others are default "*"), and put HorizontalAlignment="Left" on your WrapPanel and HorizontalAlignment="Right" on the ListBox for phone numbers. You may have to alter that ListBox a bit to get the phone numbers even more right-aligned, such as creating a DataTemplate for them.
If you want to use a Grid, then you need to change your ColumnDefinitions to be:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
If you don't need to use a Grid, then you could use a DockPanel:
<DockPanel>
<WrapPanel DockPanel.Dock="Left">
<!--Some content here-->
<TextBlock Text="{Binding Path=LastName}" TextWrapping="Wrap" FontSize="24"/>
<TextBlock Text=", " TextWrapping="Wrap" FontSize="24"/>
<TextBlock Text="{Binding Path=FirstName}" TextWrapping="Wrap" FontSize="24"/>
</WrapPanel>
<ListBox DockPanel.Dock="Right" ItemsSource="{Binding Path=PhoneNumbers}"
Margin="8,0" Background="Transparent" BorderBrush="Transparent" IsHitTestVisible="False"/>
<TextBlock />
</DockPanel>
Notice the TextBlock at the end. Any control with no "DockPanel.Dock" defined will fill the remaining space.
Taeke's answer works well, and as per vancutterromney's answer you can disable the horizontal scrollbar to get rid of the annoying size mismatch. However, if you do want the best of both worlds--to remove the scrollbar when it is not needed, but have it automatically enabled when the ListBox becomes too small, you can use the following converter:
/// <summary>
/// Value converter that adjusts the value of a double according to min and max limiting values, as well as an offset. These values are set by object configuration, handled in XAML resource definition.
/// </summary>
[ValueConversion(typeof(double), typeof(double))]
public sealed class DoubleLimiterConverter : IValueConverter
{
/// <summary>
/// Minimum value, if set. If not set, there is no minimum limit.
/// </summary>
public double? Min { get; set; }
/// <summary>
/// Maximum value, if set. If not set, there is no minimum limit.
/// </summary>
public double? Max { get; set; }
/// <summary>
/// Offset value to be applied after the limiting is done.
/// </summary>
public double Offset { get; set; }
public static double _defaultFailureValue = 0;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null || !(value is double))
return _defaultFailureValue;
double dValue = (double)value;
double minimum = Min.HasValue ? Min.Value : double.NegativeInfinity;
double maximum = Max.HasValue ? Max.Value : double.PositiveInfinity;
double retVal = dValue.LimitToRange(minimum, maximum) + Offset;
return retVal;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Then define it in XAML according to the desired max/min values, as well an offset to deal with that annoying 2-pixel size mismatch as mentioned in the other answers:
<ListBox.Resources>
<con:DoubleLimiterConverter x:Key="conDoubleLimiter" Min="450" Offset="-2"/>
</ListBox.Resources>
Then use the converter in the Width binding:
<Grid.Width>
<Binding Path="ActualWidth" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type ScrollContentPresenter}}" Converter="{StaticResource conDoubleLimiter}" />
</Grid.Width>
The method in Taeke's answer forces a horizontal scroll bar. This can be fixed by adding a converter to reduce the grid's width by the width of the vertical scrollbar control.
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
namespace Converters
{
public class ListBoxItemWidthConverter : MarkupExtension, IValueConverter
{
private static ListBoxItemWidthConverter _instance;
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return System.Convert.ToInt32(value) - SystemParameters.VerticalScrollBarWidth;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
public override object ProvideValue(IServiceProvider serviceProvider)
{
return _instance ?? (_instance = new ListBoxItemWidthConverter());
}
}
}
Add a namespace to the root node of your XAML.
xmlns:converters="clr-namespace:Converters"
And update the Grid width to use the converter.
<Grid.Width>
<Binding Path="ActualWidth" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type ScrollContentPresenter}}" Converter="{converters:ListBoxItemWidthConverter}"/>
</Grid.Width>

Resources