I couldn't find a solution for my problem/idea, and I hope that someone could help me out.
In WPF I have a CollectionViewSource depending on an IEnumerable<Item>.
An Item has the Name, Height, Age fields.
In Xaml, the ListView's ItemsSource="{Binding CollectionViewSource.View}".
I know, the Listview has an AlternationCount property which can change the row background color.
But I want to change the row background color only if the Age field data is different from the above row's Age data.
Like this, ordered by Age:
The row background color is alternated only when the Age data is different.
When I set another sort order to the list, the alternating should also be changed.
In this picture the list is ordered by Name:
But the background color depends on the Age data.
Is there any way to make a solution for this concept?
You can use the RelativeSource.PreviousData for your task.
First, create an IMultivalueConverter which will accept the values you want to compare and return the current alternation index based on them:
class ComparisonConverter : IMultiValueConverter
{
private int currentAlternation = 0;
public int AlternationCount { get; set; } = 2;
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// TODO: exception handling
if (values.Distinct().Count() != 1)
{
if (++currentAlternation >= AlternationCount)
{
currentAlternation = 0;
}
}
return currentAlternation;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
This converter accepts multiple values and returns an unchanged alternation index when all values are equal; otherwise, it first changes the alternation index to the next one and then returns a new index.
Now, create a MultiBinding that will provide the alternation index value to the Style, where you define your colors:
<!-- This is an incomplete ListView! Set the View and ItemsSource as required. -->
<ListView>
<ListView.Resources>
<local:ComparisonConverter x:Key="ComparisonConverter"/>
</ListView.Resources>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Style.Triggers>
<!-- This is the DataTrigger for the alternation index 1 -->
<DataTrigger Value="1">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource ComparisonConverter}">
<Binding Path="Age"/>
<Binding Path="Age" RelativeSource="{RelativeSource PreviousData}"/>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Background" Value="Red"/>
</DataTrigger>
</Style.Triggers>
<Setter Property="Background" Value="Wheat"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
So in this style, the default Color for the alternation index 0 is Wheat. Using the DataTrigger, the alternation index 1 will generate the color Red.
The sort order changes will be reflected automatically, because the CollectionViewSource will rebuild the view, so the ListView will create all items from scratch using the MultiBinding for each item.
Run this and live life to fullest :
XAML :
<Window x:Class="WpfStackOverflow.Window6"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window6" Height="362.03" Width="563.91">
<Window.Resources>
<CollectionViewSource x:Key="CVS" Source="{Binding .}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Age"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</Window.Resources>
<Grid>
<ListView x:Name="LstView" ItemsSource="{Binding Source={StaticResource CVS}}">
<ListView.Resources>
<AlternationConverter x:Key="AltCnvKey">
<SolidColorBrush Color="Snow"/>
<SolidColorBrush Color="LightBlue"/>
<SolidColorBrush Color="Orange"/>
</AlternationConverter>
</ListView.Resources>
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn DisplayMemberBinding="{Binding Name}" Header="Name"/>
<GridViewColumn DisplayMemberBinding="{Binding Height}" Header="Height"/>
<GridViewColumn DisplayMemberBinding="{Binding Age}" Header="Age"/>
</GridView.Columns>
</GridView>
</ListView.View>
<ListView.GroupStyle>
<GroupStyle AlternationCount="3">
<GroupStyle.ContainerStyle>
<Style TargetType="GroupItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GroupItem">
<StackPanel Orientation="Horizontal">
<StackPanel.Resources>
<Style TargetType="StackPanel">
<Setter Property="Background" Value="{Binding RelativeSource={RelativeSource AncestorType=GroupItem, Mode=FindAncestor}, Path=(ItemsControl.AlternationIndex), Converter={StaticResource AltCnvKey}}"/>
</Style>
</StackPanel.Resources>
<ItemsPresenter/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
</Grid>
Code :
using System.Linq;
using System.Windows;
namespace WpfStackOverflow
{
/// <summary>
/// Interaction logic for Window6.xaml
/// </summary>
public partial class Window6 : Window
{
public Window6()
{
InitializeComponent();
this.DataContext = new[] { new { Age = 32, Name = "Name1", Height = 6 }, new { Age = 34, Name = "Name1", Height = 6 }, new { Age = 34, Name = "Name1", Height = 6 }, new { Age = 32, Name = "Name1", Height = 6 }, new { Age = 32, Name = "Name1", Height = 6 }, new { Age = 39, Name = "Name1", Height = 6 }, new { Age = 40, Name = "Name1", Height = 6 } }.ToList();
}
}
}
Related
The Problem
I'm new to WPF and trying to learn. I have a basic ListView showing info about people such as Name, Age and Grade.
I want the Grade result text to be green if enum is "Pass" and red if "Fail", otherwise the text colour isn't changed.
What I tried
I know you can hard code all text in a column to be green, red etc with Foreground="" but this wouldn't work.
I tried implementing a function that checks if each enum in the list equals Pass etc but I couldn't get it and I'm quite stuck here.
XAML
<Grid Margin="10">
<ListView Name="lvUsers">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{Binding Age}" />
<GridViewColumn Header="Grade" Width="100" DisplayMemberBinding="{Binding Grade}"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
CS
public partial class MainWindow : Window
{
public class User
{
public string Name { get; set; }
public int Age { get; set; }
public Grade Grade { get; set; }
}
public MainWindow()
{
InitializeComponent();
List<User> items = new List<User>();
items.Add(new User() { Name = "John Doe", Age = 42, Grade = Grade.fail });
items.Add(new User() { Name = "Jane Doe", Age = 39, Grade = Grade.pass });
items.Add(new User() { Name = "Sammy Doe", Age = 13, Grade = Grade.fail });
lvUsers.ItemsSource = items;
}
public enum Grade
{
none = 0,
pass = 1,
fail = 2
};
}
Expected result
I'm don't want to have all text in Grade column to be green/red. And I don't want to add a Colour property inside the User Class.
When the enum value is "Pass" for the User, the "Pass" text in the Grade column will be green. When it's "Fail", the text will be red. Otherwise, text colour is not changed.
Any help is much appreciated, because I'm quite stuck here.
You actually have a multitude of options to use here:
Firstly you will need to replace this GridViewColumn entry with the examples in one of the following sections:
<GridViewColumn Header="Grade" Width="100" DisplayMemberBinding="{Binding Grade}"/>
1 - DataTrigger
MSDN documentation here
This will work but is not reusable.
<!-- A custom cell template lets you customise how the cell will display -->
<GridViewColumn Header="Grade" Width="10">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Name="GradeText" Text="{Binding Grade}" />
<!-- define rules on how the ui will change based on the data bound -->
<DataTemplate.Triggers>
<!-- see NOTE below for how to get this working -->
<DataTrigger Binding="{Binding Grade}" Value="{x:Static enum:Grade.pass}">
<Setter TargetName="GradeText" Property="Foreground" Value="Green"/>
</DataTrigger>
<!-- you can add a second one for fail ;) -->
</DataTemplate.Triggers>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
NOTE you will also need to add a namespace declaration to where you enum is declared (full credit to this answer):
xmlns:enum="clr-namespace:YourEnumNamespace;assembly=YourAssembly"
2 - Converter
MSDN documentation here
This provides the least amount of XAML, allows you to reuse the logic elsewhere and if you really care about it, unit test the converter logic.
<!-- A custom cell template lets you customise how the cell will display -->
<GridViewColumn Header="Grade" Width="10">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Name="GradeText" Text="{Binding Grade}" Foreground="{Binding Grade, Converter={StaticResource GradeToBrushConverter}}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
And the converter code:
public GradeToBrushConverter : IValueConverter
{
public object Convert (object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is Grade grade)
{
switch (grade)
{
case Grade.pass:
return Brushes.Green;
case fail:
return Brushes.Red;
default:
return Brushes.Black; // Or a more sensible default.
}
}
return Brushes.Black;
}
// I haven't provided the ConvertBack but you should be able to work this bit out.
}
3 - Styling
Yes I appreciate that this example looks quite similar to point 1 but it has the added benefit that if you declare the style somewhere else it could be reused in multiple places.
<!-- A custom cell template lets you customise how the cell will display -->
<GridViewColumn Header="Grade" Width="10">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Name="GradeText" Text="{Binding Grade}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="Black"/>
<Style.Triggers>
<!-- see NOTE below for how to get this working -->
<DataTrigger Binding="{Binding Grade}" Value="{x:Static enum:Grade.pass}">
<Setter Property="Foreground" Value="Green"/>
</DataTrigger>
<!-- you can add a second one for fail ;) -->
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
I'm trying to further customize build-in capability of WPF ListBox for showing items in groups.
In short, I want to hide Group's container (and Group's title altogether) if all items inside group are collapsed (Visibility property).
First, I have very simple class City that represent single Item. This class include Shown property. Inside ItemContainerStyle I simply have DataTrigger that set Visibility to Collapsed if value of this property is False.
class City : INotifyPropertyChanged
{
private bool m_Shown = true;
public string Name { get; set; }
public string Country { get; set; }
public bool Shown
{
get
{
return m_Shown;
}
set
{
m_Shown = value;
PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Shown"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
This is how I add sample cities, add Group description and all work fine.
m_cities = new List<City>
{
new City() { Name = "Berlin", Country = "Germany" },
new City() { Name = "Milano", Country = "Italy" },
new City() { Name = "Frankfurt", Country = "Germany" },
new City() { Name = "Rome", Country = "Italy" }
};
ICollectionView view = CollectionViewSource.GetDefaultView(m_cities);
view.GroupDescriptions.Add(new PropertyGroupDescription("Country"));
Cities = view; // <-- Binds to ItemsSource of ListBox
I tried in several ways to automatically hide Group if there are no more items visible in it (all are collapsed), but all without luck.
One way is to repeat last 3 lines in code above and this works, but I noticed slowdown with this method and listbox must work fast for user.
Bellow is one of my examples and this actually worked for hiding, but I can't bring group to be visible anymore after that. I tried with converters and similar, but I can't get group visible again.
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Style.Triggers>
<Trigger Property="ActualHeight" Value="20">
<Setter Property="Visibility" Value="Collapsed"/>
</Trigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Visible"/>
<Setter Property="MinHeight" Value="20"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<StackPanel>
<TextBlock Text="{Binding Path=Name}"/>
<ItemsPresenter/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListBox.GroupStyle>
Thanks for any help.
A bit (!) late with this, but hopefully it might help someone else in the future.
Inside the control template of most (all?) GroupItem styles is an ItemsPresenter that is used to host and display the child items that belong to the group. It stands to reason that, if all of the child items are collapsed, this ItemsPresenter will have a height of zero.
Therefore, you can add a trigger to the control template based on this condition, and set the Visibility of the whole group item accordingly. A normal property trigger doesn't seem to work, but a data trigger will. Something like this:
<ControlTemplate>
<StackPanel x:Name="Root">
...
<ItemsPresenter x:Name="ItemsPresenter" />
...
</StackPanel>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding ActualHeight, ElementName=ItemsPresenter}" Value="0">
<Setter TargetName="Root" Property="Visibility" Value="Collapsed" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
You will need to name the root element of the control template (in this example it's a StackPanel element I've named "Root") and also the ItemsPresenter element (I've just called it "ItemsPresenter"). Obviously the root element might be a different type, and you can use whichever names you like.
You were on the right track, but you needed to bind to the ActualHeight of the ItemsPresenter, and it needed to be a data trigger not a normal property trigger.
I am creating a wpf app and struggling with this problem for sometime now.
I have a datagrid with DataGridTemplateColumn, which contains a checkbox and textblock.
<DataGrid
Name="ChargeDataGrid"
Grid.Row="1"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox x:Name="CheckBox1"/>
<TextBlock Text="{Binding}" />
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.HeaderTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox />
<TextBlock Text="Title" />
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.HeaderTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
<system:String>123</system:String>
<system:String>124</system:String>
<system:String>125</system:String>
<system:String>126</system:String>
<system:String>127</system:String>
</DataGrid>
What i need to achieve is when row is clicked checkbox in this row must be in checked state too.
I tried to use style triggers:
<Style TargetType="{x:Type DataGridCell}">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="CheckBox1.IsChecked" Value="True" />
<Setter Property="Background" Value="Blue" />
</Trigger>
</Style.Triggers>
</Style>
but it didn't seem possible to change checkbox state like this. I know how to do it in code-behind or mvvm style, but in this case i am wondering is it possible to do using xaml only?
Any help would be appreciated.
I am afraid but with plain standard XAML you can't do it.
As I see you have two options:
You can use some extension libraries which will expand functionality of bindings. Some functionality can be found in mvvm frameworks like MugenMvvmToolkit
Second option is to use some converter for this purpose.
My solution for the second variant is a kind of hack and to my mind more elegant way would be with code behind. Converter:
public class MultiValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length != 3) throw new ArgumentException("Should be 3 params");
if (!(values[2] is FrameworkElement element)) return values[1];
if (!(bool)values[0])
{
element.Tag = "Value need to be changed.";
return values[1];
}
if (element.Tag.Equals("Value changed.")) return values[1];
var res = !(bool)(values[1] ?? true);
element.Tag = "Value changed.";
return res;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Converter inversing bool variable and saves previous state in Tag property of control. This is still not code behind and pretty reusable solution. You can use such converter in any other view where you need such behaviour
In XAML I've changed only checkbox control definition:
<CheckBox x:Name="RowCheckBox" IsHitTestVisible="False">
<CheckBox.IsChecked>
<MultiBinding Converter="{StaticResource MultiValueConverter}" Mode="OneWay">
<Binding Path="IsSelected" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type DataGridRow}}" />
<Binding Path="IsChecked" RelativeSource="{RelativeSource Self}" />
<Binding Mode="OneWay" RelativeSource="{RelativeSource Self}" />
</MultiBinding>
</CheckBox.IsChecked>
</CheckBox>
I think it can bed done rather simple like this:
<CheckBox x:Name="CheckBox1" IsChecked="{Binding RelativeSource={RelativeSource AncestorType=DataGridRow}, Path=IsSelected}"/>
EDIT:
If the desired result is to only change the IsChecked state when (re)selecting the row it can be done with a attached property on a DependencyObject (for instance the containing window) like this:
1) Define the checkbox as this:
<CheckBox x:Name="CheckBox1" IsEnabled="true" local:MainWindow.CheckboxChecked="{Binding RelativeSource={RelativeSource AncestorType=DataGridRow}, Path=IsSelected,Mode=OneWay}">
2) Define the attached property as this:
public static bool GetCheckboxChecked(DependencyObject obj)
{
return (bool)obj.GetValue(CheckboxCheckedProperty);
}
public static void SetCheckboxChecked(DependencyObject obj, bool value)
{
obj.SetValue(CheckboxCheckedProperty, value);
}
public static readonly DependencyProperty CheckboxCheckedProperty =
DependencyProperty.RegisterAttached("CheckboxChecked", typeof(bool), typeof(MainWindow), new PropertyMetadata(false, CheckboxChecked_Changed));
private static void CheckboxChecked_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
CheckBox chk = d as CheckBox;
if (chk != null && chk.Tag == null)
{
bool chkValue = chk.IsChecked.GetValueOrDefault();
bool oldValue = (bool)e.OldValue;
bool newValue = (bool)e.NewValue;
chk.Tag = true; // Just to prevent an infinite loop
chk.IsChecked = !chkValue && !newValue || chkValue && !oldValue && newValue ? false : true;
chk.Tag = null;
}
}
I'm having trouble with finding the solution for the problem, namely I had an idea to color each row/column of combobox with different colors, depending on the area, but i cannot find any clues or hints or instructions to do so. the app is pretty simple
<ComboBox x:Name="comboBox1" HorizontalAlignment="Left" Margin="84,70,0,0" VerticalAlignment="Top" Width="230"/>
this is my XAML combobox, which i fill from the code:
SortedList<int, string> AreaList = new SortedList<int, string>();
AreaList.Add(1, "Agriculture");
AreaList.Add(2, "Forestry");
AreaList.Add(3, "Fruits");
AreaList.Add(4, "Food");
AreaList.Add(5, "Metals");
AreaList.Add(6, "Mining");
AreaList.Add(7, "Electricity");
AreaList.Add(8, "Building Contracts");
AreaList.Add(9, "Transport");
AreaList.Add(10, "Alcohol");
AreaList.Add(11, "Information Technologies");
AreaList.Add(12, "Health And Social Services");
AreaList.Add(13, "Art and Entertainement");
AreaList.Add(14, "Hospitality Business");
AreaList.Add(15, "Education");
AreaList.Add(16, "Real Estate");
AreaList.Add(17, "Sales");
AreaList.Add(18, "Architecture");
AreaList.Add(19, "Engineering");
AreaList.Add(20, "Wholesale");
AreaList.Add(21, "Other");
comboBox1.ItemsSource = AreaList.ToList();
comboBox1.SelectedValuePath = "Key";
comboBox1.DisplayMemberPath = "Value";
each of these items have their color in another window, but i would like to show those colors in the combobox, the background of "Agriculture" row/column should be green etc.
Is there a solution to this, or do i have to redo it all over?
You could use an ItemContainerStyle with a DataTrigger for each value that maps to a colour:
<ComboBox x:Name="comboBox1">
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Style.Triggers>
<DataTrigger Binding="{Binding Value}" Value="Agriculture">
<Setter Property="Background" Value="Green" />
</DataTrigger>
<DataTrigger Binding="{Binding Value}" Value="Forestry">
<Setter Property="Background" Value="Red" />
</DataTrigger>
<!-- and so on-->
</Style.Triggers>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
You may also want to read this:
Changing the background colour of a ComboBox in WPF: https://blog.magnusmontin.net/2014/04/30/changing-the-background-colour-of-a-combobox-in-wpf-on-windows-8/
You can make use of ItemContainerStyle and Converter
public class StringToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (((KeyValuePair<int, string>)value).Value.ToString() == "Agriculture")
return Brushes.Green;
//and so on or other ways to get the color
return Brushes.Transparent;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
and XAML goes as follows,
<Window.Resources>
<local:StringToColorConverter x:Key="StringToColorConverter"/>
</Window.Resources>
<Grid >
<ComboBox x:Name="comboBox1" HorizontalAlignment="Left" Margin="84,70,0,0" VerticalAlignment="Top" Width="230">
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="Background" Value="{Binding Converter={StaticResource StringToColorConverter}}">
</Setter>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
</Grid>
At the moment I have a date time axis where the date is in-line with the points, is there anyway to get this date to appear in the center such as on a bar chart.
<Style x:Key="DateTimeAxisLabelStyle2" TargetType="chartingToolkit:DateTimeAxisLabel">
<Setter Property="DaysIntervalStringFormat" Value="{}{0:dd-MMM}" />
<Setter Property="HoursIntervalStringFormat" Value="{}{0:hh:mm tt}" />
<!--<Setter Property="RenderTransformOrigin" Value="1,0.5" />
<Setter Property="RenderTransform">
<Setter.Value>
<RotateTransform Angle="-45" />
</Setter.Value>
</Setter>-->
<!--<Setter Property="Margin" Value="30,0,-10,0" />-->
</Style>
<chartingToolkit:DateTimeAxis IntervalType="Days"
Interval="1"
Minimum="{Binding StartDate}"
Maximum="{Binding EndDate}"
Orientation="X"
VerticalContentAlignment="Center"
Title="Day"
AxisLabelStyle="{StaticResource DateTimeAxisLabelStyle2}" />
Any help would be greatly appreciated.
Here's what i got:
XAML:
<Window.Resources>
<Style x:Key="DateTimeAxisLabelStyle1" TargetType="{x:Type chartingToolkit:DateTimeAxisLabel}">
<Setter Property="DaysIntervalStringFormat" Value="{}{0:dd-MMM}"></Setter>
<Setter Property="RenderTransformOrigin" Value="0.80,0.20"></Setter>
<Setter Property="RenderTransform">
<Setter.Value>
<RotateTransform Angle="-90"></RotateTransform>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<chartingToolkit:Chart Margin="0" Title="Chart Title">
<chartingToolkit:Chart.DataContext>
<local:MyDataCollection/>
</chartingToolkit:Chart.DataContext>
<chartingToolkit:Chart.Axes>
<chartingToolkit:DateTimeAxis Minimum="{Binding StartDate}" Maximum="{Binding EndDate}" Orientation="X" ShowGridLines="True" AxisLabelStyle="{DynamicResource DateTimeAxisLabelStyle1}"/>
</chartingToolkit:Chart.Axes>
<chartingToolkit:LineSeries DependentValuePath="Y" IndependentValuePath="X" ItemsSource="{Binding}"/>
</chartingToolkit:Chart>
</Grid>
Chart:
Here's what I did using the WPF Toolkit Source for reference.
I created a custom class deriving from DateTimeAxis, then overrode the "GetPlotAreaCoordinate" method. The DateTimeAxis.Render() calls that method three times with the same list of "DateTime" values, once for the MajorTickmarks, once for MinorTickmarks, and once for the date label. There were no minor tickmarks in the list, so the method was actually only getting called twice. I just keep a list of the values that have been evaluated and assume that if it's in the list it's already done the tickmarks and is now doing the Labels.
class CustomDateTimeAxis : DateTimeAxis
{
List<object> _valueList = new List<object>();
UnitValue prevBaseValue;
protected override UnitValue GetPlotAreaCoordinate(object value, Range<IComparable> currentRange, double length)
{
_valueList.Add(value);
UnitValue baseValue = base.GetPlotAreaCoordinate(value, currentRange, length);
int valueCount = _valueList.Count((x) => x.Equals(value));
if (valueCount == 2)
return new UnitValue(baseValue.Value + 27, baseValue.Unit);
prevBaseValue = baseValue;
return baseValue;
}
protected override void Render(Size availableSize)
{
base.Render(availableSize);
_valueList.Clear();
}
}
"27" is just a number I was trying out. You might want to play with that to see what works best for you.
return new UnitValue(baseValue.Value + 27, baseValue.Unit);
I created a Margin-Converter:
public class MarginConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var dateTimeAxis = values[0] as DateTimeAxis; ;
var actualAxisLength = values[1] as double?;
var actualMaximum = values[2] as DateTime?;
var actualMinimum = values[3] as DateTime?;
if (dateTimeAxis == null ||
!dateTimeAxis.Interval.HasValue ||
!actualAxisLength.HasValue ||
!actualMaximum.HasValue ||
!actualMinimum.HasValue)
return null;
double xMargin = 0;
var interval = dateTimeAxis.Interval.Value;
var timeSpan = actualMaximum.Value - actualMinimum.Value;
var timeSpanInDays = timeSpan.TotalDays;
if (dateTimeAxis.IntervalType == DateTimeIntervalType.Months)
{
xMargin = 30 * interval * actualAxisLength.Value / timeSpanInDays;
}
else if (dateTimeAxis.IntervalType == DateTimeIntervalType.Days)
{
xMargin = interval * actualAxisLength.Value / timeSpanInDays;
}
return new Thickness(xMargin, 10, 0, -30);
}
public object[] ConvertBack(object value, System.Type[] targetType, object parameter, CultureInfo culture)
{
return null;
}
}
called the X-Axis 'SharedXAxis' and used the converter like this:
<Setter Property="Margin">
<Setter.Value>
<MultiBinding Converter="{StaticResource MarginConv}">
<Binding ElementName="SharedXAxis"/>
<Binding ElementName="SharedXAxis" Path="ActualWidth"/>
<Binding ElementName="SharedXAxis" Path="ActualMaximum"/>
<Binding ElementName="SharedXAxis" Path="ActualMinimum"/>
</MultiBinding>
</Setter.Value>
Imo this should be full dynamic.
The Top- and Bottom-Values of the Thickness of the Margin-Converter ('10' and '-30' in my case) as well as the Bottom-Value of the Padding of the Chart itself have to be adjusted, I don't know why.