WPF own bar chart using grid - wpf

Let's say we have 25 products A and 14 products B. I want to create chart that is representing them using rectangles and grid. I wrote this code below and it works, but the chart generated with it is very inaccurate. Any ideas how to fix it?
<StackPanel Orientation="Horizontal">
<!--Products A-->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="0.2*"/>
<RowDefinition Height="percentage1*"/>
</Grid.RowDefinitions>
<Rectangle Fill="Red" HorizontalAlignment="Stretch" Grid.Row="1"/>
</Grid>
<!--Products B-->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="0.2*"/>
<RowDefinition Height="percentage2*"/>
</Grid.RowDefinitions>
<Rectangle Fill="Red" HorizontalAlignment="Stretch" Grid.Row="1"/>
</Grid>
</StackPanel>
percentage1 = 25 products / (25 products + 14 products)
percentage2 = 14 products / (25 products + 14 products)

You should not control the size of the Rectangle with the width of a Grid row. Instead host the Rectangle elements in a ItemsControl and calculate the final rendered width based on the available space:
rectangle_width = ratio * available_width
= value / max_value_in_chart * available_width
It's best to move the logic and layout to a custom Control or UserControl. And because the Rectangle bars are hosted in an ItemsControl, you can as many bars as you need to without any hassle (opposed to modifying a Grid to add more rows):
Usage
<local:SimpleBarChart x:Name="BarChart"
BarThickness="64"
Orientation="Horizontal"/>
public MainWindow()
{
InitializeComponent();
// Better use data binding
this.BarChart.BarValues = new List<double> { 12, 24, 36 };
}
SimpleBarChart.xaml.cs
public partial class SimpleBarChart : UserControl
{
public IList<double> BarValues
{
get => (IList<double>)GetValue(BarValuesProperty);
set => SetValue(BarValuesProperty, value);
}
public static readonly DependencyProperty BarValuesProperty = DependencyProperty.Register(
"BarValues",
typeof(IList<double>),
typeof(SimpleBarChart),
new PropertyMetadata(default));
public double BarThickness
{
get => (double)GetValue(BarThicknessProperty);
set => SetValue(BarThicknessProperty, value);
}
public static readonly DependencyProperty BarThicknessProperty = DependencyProperty.Register(
"BarThickness",
typeof(double),
typeof(SimpleBarChart),
new PropertyMetadata(default));
public SimpleBarChart()
{
InitializeComponent();
}
}
SimpleBarChart.xaml
<UserControl>
<UserControl.Resources>
<local:BarValueToLengthConverter x:Key="BarValueToLengthConverter" />
</UserControl.Resources>
<ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=BarValues}"
HorizontalAlignment="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=HorizontalContentAlignment}"
HorizontalContentAlignment="Stretch"
VerticalAlignment="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=VerticalalContentAlignment}"
VerticalContentAlignment="Stretch">
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Margin"
Value="0,0,0,12" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle Fill="Orange"
Height="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=BarThickness}"
HorizontalAlignment="Left">
<Rectangle.Width>
<MultiBinding Converter="{StaticResource BarValueToLengthConverter}">
<Binding RelativeSource="{RelativeSource AncestorType=UserControl}"
Path="BarValues" />
<Binding />
<Binding RelativeSource="{RelativeSource AncestorType=UserControl}"
Path="BarThickness" />
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}"
Path="ActualWidth" />
</MultiBinding>
</Rectangle.Width>
</Rectangle>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</UserControl>
BarValueToLengthConverter.cs
public class BarValueToLengthConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
IList<double>? barValues = values.OfType<IList<double>>().FirstOrDefault();
IList<double>? doubleValues = values.OfType<double>().ToList();
double barValue = doubleValues[0];
double barThickness = doubleValues[1];
double barHostWidth = doubleValues[2];
return barValue / barValues.Max() * barHostWidth;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}

Related

Unable to center a canva inside a canva

I've a view in WPF where I need to center a canvas within a canvas.
I know it's not the most suitable container for this, but we have other components that will come in this canvas where it will simplify a lot our job.
So basically, I've currently this code:
<Canvas Name="RootContainer" Background="#373B3F" ClipToBounds="True" MouseLeftButtonDown="OnMainCanvasMouseLeftButtonDown">
<Canvas.DataContext>
<local:SomeViewModel/>
</Canvas.DataContext>
<Canvas Name="SomeContainer" Background="#373B3F" MouseMove="OnCanvasMouseMove" MouseWheel="OnCanvasMouseWheel">
<Canvas.Left>
<MultiBinding Converter="{StaticResource CenterValueConverter}">
<Binding ElementName="RootContainer" Path="ActualWidth" />
<Binding ElementName="SomeContainer" Path="ActualWidth" />
</MultiBinding>
</Canvas.Left>
<Canvas.Top>
<MultiBinding Converter="{StaticResource CenterValueConverter}">
<Binding ElementName="RootContainer" Path="ActualHeight" />
<Binding ElementName="SomeContainer" Path="ActualHeight" />
</MultiBinding>
</Canvas.Top>
<ItemsControl ItemsSource="{Binding SomeChilds}" Name="ItemsControl">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Left}" />
<Setter Property="Canvas.Top" Value="{Binding Top}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding}" ContentTemplateSelector="{StaticResource ConventionBasedDataTemplateSelector}"
MouseLeftButtonDown="OnMouseLeftButtonDown" PreviewMouseLeftButtonUp="OnPreviewMouseLeftButtonUp" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Some other controls over here -->
</Canvas>
</Canvas>
And this converter:
public class CenterValueConverter : IMultiValueConverter
{
public object Convert(object[] values,
Type targetType,
object parameter,
CultureInfo culture)
{
if (values == null || values.Length != 2)
{
return null;
}
double totalWidth = (double)values[0];
double size = (double)values[1];
return totalWidth / 2 - (size / 2);
}
public object[] ConvertBack(object value,
Type[] targetTypes,
object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
The issue is that the second value(SomeContainer.ActualWidth/ActualHeight) always comes with a value of 0(even when I've some real elements it).
Any idea why and how to fix this? Or another XAML way of centering SomeContainer inside of the RootContainer ?
EDIT
Maybe some additional informations on why I planned to use so much Canvas.
The RootContainer one is because SomeContainer will have transformation(scale for zooming) and translation for panning
The SomeContainer could be something different I guess
The Canvas inside the ItemControls is because each elements will be positioned as a very specific place.
I guess your ActualHeight/ActualWidth are = 0 because you didn't load the window yet.
To make these calculous taking in account dimensions of Canvas you may use 'OnContentRendered' event, that will be launched after the window is displayed, so you will have their dimensions appearing. If your Canvas may change dimensions, you may also use SizeChanged event.
Even I think it is a bit complicated, and you could just use that XAML code taken from Clemen's answer :
<Canvas Background="Transparent"
SizeChanged="ViewportSizeChanged"
MouseLeftButtonDown="ViewportMouseLeftButtonDown"
MouseLeftButtonUp="ViewportMouseLeftButtonUp"
MouseMove="ViewportMouseMove"
MouseWheel="ViewportMouseWheel">
<Canvas x:Name="canvas" Width="1000" Height="600">
<Canvas.RenderTransform>
<MatrixTransform x:Name="transform"/>
</Canvas.RenderTransform>
<Ellipse Fill="Red" Width="100" Height="100" Canvas.Left="100" Canvas.Top="100"/>
<Ellipse Fill="Green" Width="100" Height="100" Canvas.Right="100" Canvas.Bottom="100"/>
</Canvas>
</Canvas>

Drawing Ellipse Line/PolyLine with a double

I have been working with this example to draw bar graph. The bar part is done and it represents the volume/quantity in a transaction. It's an associated price in range 20 - 30. What I want now is to draw points to represent price associated with volumes and connect those points. Two changes I've made in EDIT part of the linked example (1) removed the TextBlock from the DataTemplate of ItemsControl and added an Ellipse instead and (2) edited the canvas to add price/volume axis label. Here's how it looks like now:
How to add those Ellipse in right position and connect those with Line/PolyLine?
EDIT
Here's what I've now in ItemsControl:
<ItemsControl ScrollViewer.CanContentScroll="True"
Height="135"
ItemsSource="{Binding RectCollection}"
Margin="50 0 50 0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas Width="20">
<Canvas.LayoutTransform>
<ScaleTransform ScaleY="-1"/>
</Canvas.LayoutTransform>
<Rectangle Width="18"
Margin="0 0 2 0"
VerticalAlignment="Bottom"
Opacity=".5" Fill="LightGray">
<Rectangle.Height>
<MultiBinding Converter="{StaticResource VConverter}">
<Binding Path="ActualHeight"
RelativeSource="{RelativeSource AncestorType=ItemsControl}"/>
<Binding Path="DataContext.HighestPoint"
RelativeSource="{RelativeSource AncestorType=ItemsControl}"/>
<Binding Path="Volume"/>
</MultiBinding>
</Rectangle.Height>
</Rectangle>
<Line Stroke="DarkGreen" StrokeThickness="1"
X1="10" X2="30"
Y2="{Binding PreviousPrice, Converter={StaticResource PConverter}}"
Y1="{Binding CurrentPrice, Converter={StaticResource PConverter}}">
<Line.Style>
<Style TargetType="Line">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=PreviousPrice}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Line.Style>
</Line>
<Ellipse Fill="Red" Width="6" Height="6" Margin="-3" Canvas.Left="10"
Canvas.Top="{Binding CurrentPrice, Converter={StaticResource PConverter}}"/>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.Template>
<ControlTemplate>
<ScrollViewer
VerticalScrollBarVisibility="Hidden"
Background="{TemplateBinding Panel.Background}">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
oh! I forgot to add those ValueConverters, here're those:
public class VolumeConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var height = (double)values[0];
var higest = (double)values[1];
var value = (double)values[2];
return value * height / higest;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class PriceConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(value is double)) return null;
var price = (double)value;
var remainingHeight = 90;
var priceRange = 30 - 20.0;
return 45 + ((price - 20) * remainingHeight / priceRange);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
and here's how it looks like:
As suggested by #Clemens, I've to have another double?, in case where Insert(0, ... ) is used on ObservableCollection instead of Add(...) to add the last item in first place and removed the AternationCount/Index stuff.
The following example uses a vertically flipped Canvas to invert the y-axis order, so that it goes upwards. So PConverter should return positive y values.
Besides the Rectangle and Ellipse elements it draws a Line element from the previous data value to the current one by means of RelativeSource={RelativeSource PreviousData} in the value binding. It also uses a DataTrigger on the AlternationIndex to hide the first line.
<ItemsControl ... AlternationCount="2147483647">
...
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas Width="20">
<Canvas.LayoutTransform>
<ScaleTransform ScaleY="-1"/>
</Canvas.LayoutTransform>
<Rectangle Fill="LightGray" Margin="1" Width="18"
Height="{Binding Value1, Converter={StaticResource PConverter}}"/>
<Line Stroke="DarkGreen" StrokeThickness="3"
X1="-10" X2="10"
Y1="{Binding Price,
Converter={StaticResource PConverter},
RelativeSource={RelativeSource PreviousData}}"
Y2="{Binding Price,
Converter={StaticResource PConverter}}">
<Line.Style>
<Style TargetType="Line">
<Style.Triggers>
<DataTrigger
Binding="{Binding Path=(ItemsControl.AlternationIndex),
RelativeSource={RelativeSource
AncestorType=ContentPresenter}}"
Value="0">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Line.Style>
</Line>
<Ellipse Fill="Red" Width="6" Height="6" Margin="-3" Canvas.Left="10"
Canvas.Top="{Binding Price, Converter={StaticResource PConverter}}"/>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
Since the value converter is now also called for a non-existing value (for PreviousData of the first item), you have to make sure that it checks if the passed value is actually a double:
public object Convert(
object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(value is double)) return 0d;
...
}

WPF showing control with DataTrigger skips it in TabOrder

I would like to show 'additional details' Textbox when the previous TextBox's value is not 0.00 (zero). This is easily accomplished with DataTriggers:
<TextBox Name="FirstTB" Text="{Binding Amount, StringFormat=F2}"/><!--when this TB is not 0-->
<TextBox Name="SecondTB"> <!--show this TB-->
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding Amount}" Value="0">
<Setter Property="Visibility" Value="Hidden"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<TextBox Name="ThirdTB"/>
The issue is, however, when changing value of FirstTB to <> 0 and pressing Tab the focus jumps to the ThirdTB instead of SecondTB (even though SecondTB now is Visible due to the DataTrigger). How can I fix this issue?
Sadly, UpdateSourceTrigger=PropertyChanged does not appear to be an option due to its interference with StringFormats - it gives terrible UX when you are editing the value, carret jumps around like crazy due to constant StringFormat evaluation. Viewmodel used in example above:
public class MyVM : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private double _amount;
public double Amount
{
get { return _amount; }
set
{
_amount = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Amount)));
}
}
}
Maybe a stupid work around. But this will do the work:
View:
<TextBox Name="SecondTB" IsVisibleChanged="SecondTB_OnIsVisibleChanged">
<!--show this TB-->
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding Amount, Mode=OneWay}" Value="0">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
C#:
private void SecondTB_OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (((bool) e.NewValue))
{
if(!(sender is TextBox txbx)) return;
ThirdTB.GotFocus += ThirdTbOnGotFocus;
}
}
private void ThirdTbOnGotFocus(object sender, RoutedEventArgs e)
{
SecondTB.Focus();
ThirdTB.GotFocus -= ThirdTbOnGotFocus;
}
If you use an IValueConverter you get a very elegant way for your problem. Everytime the value is changed, the logic is called.
public class IsZeroToHiddenConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is double d && d == 0)
return Visibility.Hidden;
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
You calso play around and add ConverterParameter to decide if you want Collapsed or Hidden behavior.
Usage
<Grid>
<Grid.Resources>
<local:IsZeroToHiddenConverter x:Key="IsZeroToHiddenConverter"/>
</Grid.Resources>
<Grid VerticalAlignment="Center" HorizontalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox x:Name="Tb1" Grid.Row="0" Width="100" Margin="5" Text="{Binding Path=Amount, StringFormat=F2, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox x:Name="Tb2" Grid.Row="1" Width="100" Margin="5" Visibility="{Binding Path=Amount, Converter={StaticResource IsZeroToHiddenConverter}}"/>
<TextBox x:Name="Tb3" Grid.Row="2" Width="100" Margin="5"/>
</Grid>
</Grid>
Preview

XAML Databinding in Control templates via a dynamic Xpath expression

Ok, I'm pretty new to XAML & WPF, so I'm trying a learning project to construct a periodic table. I have a set of elemental data in XML. I have defined a control template in XAML to display this information for each chemical element as a button in a grid:
<ControlTemplate x:Key="elementTemplate" TargetType="{x:Type Button}">
<Grid>
<Rectangle x:Name="backingRect" Margin="2" StrokeThickness="0" RadiusX="3" RadiusY="3" Fill="{TemplateBinding Property=Button.Background}">
<Rectangle.Effect>
<DropShadowEffect BlurRadius="8"/>
</Rectangle.Effect>
</Rectangle>
<Viewbox>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="0.2*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="0.2*"/>
</Grid.RowDefinitions>
<TextBlock FontWeight="DemiBold" TextAlignment="Center" Grid.Row="0" diag:PresentationTraceSources.TraceLevel="High">
<TextBlock.Text>
<Binding Source="{StaticResource elementData}">
<Binding.XPath>atom[#id='H']/scalar[#dictRef='bo:atomicNumber']</Binding.XPath>
</Binding>
</TextBlock.Text>
</TextBlock>
<TextBlock FontWeight="Bold" TextAlignment="Center" FontSize="28" Grid.Row="1" >
<TextBlock.Text>
<Binding Source="{StaticResource elementData}" >
<Binding.XPath>atom[#id='H']/label[#dictRef='bo:symbol']/#value</Binding.XPath>
</Binding>
</TextBlock.Text></TextBlock >
<TextBlock FontWeight="DemiBold" TextAlignment="Center" Grid.Row="2">1.0021</TextBlock>
</Grid>
</Viewbox>
</Grid>
</ControlTemplate>
Now, this will create a button which binds to my underlying XML data statically. What I want to do is to replace the #id='H' XPath criterion with an expression that retrieves the Content of the Button that uses this template such as #id={TemplateBinding Button.Content} (if such a thing were possible), so if I create such a Button with a textual content of 'He', it will retrieve the corresponding data in the XML file such as mass, atomic number etc. What is the syntax for specifying such a dynamic XPath in XAML and binding it to a property on the control template?
See http://www.codeproject.com/Articles/854564/Parameterized-XPaths-in-XAML for a fuller discussion, ...
Essentially you have to use multi-binding with a multi-binding converter that performs the parameterized path replacement as the binding is being constructed and then return the desired content.
E.g.
<MultiBinding Converter="{StaticResource XPathConverter}" ConverterParameter="atom[#id="{1}"]/scalar[#dictRef='bo:atomicNumber']" Mode="OneWay">
<Binding Source="{StaticResource elementData}" Mode="OneWay" />
<Binding ElementName="_Button" Path="Content" Mode="OneWay"/>
</MultiBinding>
If you only need a OneWay binding, the converter is straightforward. The ConverterParamenter is the XPath with a parameterized placeholder(s). The first binding is to your XML data. And the second binding is for your control data to be used for the parameterized replacement. You will have to correct the converter below for your specific data objects and your desired return value.
public class XPathConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
XmlNode root = (value[0] as XmlNode);
string xpathf = (parameter as string);
for (int i = 1; i < values.Count(); i++)
xpathf = xpathf.Replace("{" + i.ToString().Trim() + "}", values[i] as string ?? "");
return root.SelectSingleNode(xpathf).InnerXML;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, 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