How to handle charts directly in code - c#

I started working with C# and Live Charts a few days ago.
Based on examples found in the internet, I made a very simple graph in order to understand the concepts behind it.
That is what I have (it is working perfectly fine):
public partial class Wdw_graph : Window
{
public SeriesCollection SeriesCollection { get; set; }
public string[] Labels { get; set; }
public Wdw_graph(List<dated_value> serie)
{
InitializeComponent();
SeriesCollection = new SeriesCollection();
ColumnSeries col_serie = new ColumnSeries {
Values = new ChartValues<double>(),
DataLabels = true };
Labels = new string[serie.Count];
for(int i = 0; i < serie.Count; i++)
{
col_serie.Values.Add(serie[i].value);
Labels[i] = serie[i].date;
}
SeriesCollection.Add(col_serie);
DataContext = this;
}
}
<Grid>
<lvc:CartesianChart Series="{Binding SeriesCollection}" >
<lvc:CartesianChart.AxisX>
<lvc:Axis Labels="{Binding Labels}" LabelsRotation="80">
<lvc:Axis.Separator>
<lvc:Separator IsEnabled="False" Step="1"></lvc:Separator>
</lvc:Axis.Separator>
</lvc:Axis>
</lvc:CartesianChart.AxisX>
<lvc:CartesianChart.AxisY>
<lvc:Axis Title="Sold Apps"></lvc:Axis>
</lvc:CartesianChart.AxisY>
</lvc:CartesianChart>
</Grid>
As you can see, I have 'SeriesCollection' and 'Labels' handled in the class code and them they are bound to the XAML.
I would like to know if it is possible to use the same approach to handle other graph elements, such as the 'AxisX'. If so, how can I do it?
In this post "Change the format of the axis tick labels in LiveCharts" there is a code that shows what I would like to do:
cartesianChart2.AxisX.Add(new Axis
{
Name = "xAxis",
Title = "DateTime",
FontSize = 22,
Foreground = System.Windows.Media.Brushes.Black,
MinValue = 0,
MaxValue = _amountValues,
});
But I could not reproduce that. I can't figure out where the 'cartesianChart2' comes from.

Well, I learned how to refer to the XAML elements in the class code.
I added 'x:Name="something"' to the CartesianChart and removed everything else (not using 'Binding' anymore):
<Grid>
<lvc:CartesianChart x:Name="cartesian_chart">
</lvc:CartesianChart>
</Grid>
Then, in the class code I inserted the axis and serie:
public partial class Wdw_graph : Window
{
public Wdw_graph(List<dated_value> serie)
{
InitializeComponent();
cartesian_chart.AxisX.Add(new Axis
{
Name = "xAxis",
Title = "Date and Time",
FontSize = 20,
Foreground = System.Windows.Media.Brushes.Black,
MinValue = 0,
MaxValue = serie.Count,
Labels = new String[serie.Count],
});
cartesian_chart.AxisY.Add(new Axis
{
Name = "yAxis",
Title = "Currency",
FontSize = 20,
Foreground = System.Windows.Media.Brushes.Black,
MinValue = 0,
MaxValue = 10,
});
cartesian_chart.AxisX[0].Separator.Step = 1;
cartesian_chart.AxisX[0].LabelsRotation = 80;
ColumnSeries col_serie = new ColumnSeries {
Values = new ChartValues<double>(),
DataLabels = true };
for(int i = 0; i < serie.Count; i++)
{
col_serie.Values.Add(serie[i].value);
cartesian_chart.AxisX[0].Labels[i] = serie[i].date;
}
cartesian_chart.Series.Add(col_serie);
DataContext = this;
}
}
I really preferred to handle the graph directly from the code, instead of using the XAML.
Can you tell me what are the drawbacks of this approach?

Related

LiveCharts Cartesian Mapping and Configuration with custom Labels

I have plotted the Cartesian chart using X and Y values, to mimic step chart (x values are not consistent). Is it possible to add custom x Labels for each step changed in Y value? Or similar labels like in a bar chart.
var values = new ChartValues<Point>();
Loop:
var point = new Point() { X = entry.DailyXFrom, Y = entry.YValue };
values.Add(point);
point = new Point() { X = entry.DailyXTo, Y = entry.YValue };
values.Add(point);
tempLabels.Add(entry.DailyVolume.ToString());
seriesCollectionDaily = new SeriesCollection
{
new LineSeries
{
Configuration = new CartesianMapper<Point>()
.X(point => point.X)
.Y(point => point.Y)
,
Fill = Brushes.Transparent,
Title = "Series",
Values = values,
PointGeometry = null,
LineSmoothness = 0
}
};
XAxis.Separator.IsEnabled = true;
XAxis.Labels = tempLabels.ToArray();
chart.DataContext = this;
<lvc:CartesianChart Name="chart" Grid.Row="2" Grid.ColumnSpan="2" Series="{Binding seriesCollectionDaily }" >
<lvc:CartesianChart.AxisX >
<lvc:Axis Name="XAxis" Title="" LabelsRotation="0" Foreground="Black" >
<lvc:Axis.Separator>
<lvc:Separator Name="LabelSeparator"></lvc:Separator>
</lvc:Axis.Separator>
</lvc:Axis>
</lvc:CartesianChart.AxisX>
</lvc:CartesianChart>
What I got:
This is what I'm trying to achieve. Is it possible?
You can add sections to your chart axis.
To draw vertical sections you have to define a SectionsCollection of AxisSection items and bind it to Axis.Sections of the x-axis.
By setting AxisSection.Value you can define the position on the axis. Use AxisSection.SectionWidth to define the width of the section. The default is 1, which draws a simple stroke when AxisSection.StrokeThickness is set.
The following example uses StepLineSeries to plot a step chart.
To give an example, it shows two vertical lines placed at the corresponding x-value: a simple line (SectionWidth = 1) and a section where SectionWidth > 1 (e.g., to highlight a range):
Data Model
public class DataModel : INotifyPropertyChanged
{
public DataModel()
{
this.SeriesCollection = new SeriesCollection
{
new StepLineSeries()
{
Configuration = new CartesianMapper<ObservablePoint>()
.X(point => point.X)
.Y(point => point.Y)
.Stroke(point => point.Y > 0.3 ? Brushes.Red : Brushes.LightGreen),
Values = new ChartValues<ObservablePoint>
{
new ObservablePoint(0, 5),
new ObservablePoint(20, 0),
new ObservablePoint(30, 5),
new ObservablePoint(40, 0),
new ObservablePoint(80, 5),
}
}
};
// Add two sections at x=20 and x=30
this.SectionsCollection = new SectionsCollection()
{
new AxisSection()
{
Value = 20,
Stroke = Brushes.Red,
StrokeThickness = 1
},
new AxisSection()
{
Value = 30,
SectionWidth = 50,
Stroke = Brushes.Red,
StrokeThickness = 1
}
};
}
public Func<double, string> LabelFormatter => value => $"{value}ms";
public SeriesCollection SeriesCollection { get; set; }
public SectionsCollection SectionsCollection { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Chart View
<Window>
<Window.DataContext>
<DataModel />
</Window.DataContext>
<CartesianChart Height="500"
Series="{Binding SeriesCollection}">
<CartesianChart.AxisX>
<Axis Sections="{Binding SectionsCollection}"
LabelFormatter="{Binding LabelFormatter}">
<Axis.Separator>
<Separator Step="10" />
</Axis.Separator>
</Axis>
</CartesianChart.AxisX>
<CartesianChart.AxisY>
<Axis>
<Axis.Separator>
<Separator Step="5" />
</Axis.Separator>
</Axis>
</CartesianChart.AxisY>
</CartesianChart>
</Window>

LiveCharts - Connect across missing points

I am using LiveCharts to plot several line charts on the same graph. Some of the charts have missing data points.
Current graph with gaps:
I would like to connect across these gaps:
The goal if possible:
MainWindow.xaml
<Window x:Class="LiveChartsTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:lvc="clr-namespace:LiveCharts.Wpf;assembly=LiveCharts.Wpf"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<lvc:CartesianChart Series="{Binding Series}">
<lvc:CartesianChart.AxisX>
<lvc:Axis Title="Date" Labels="{Binding Labels}"/>
</lvc:CartesianChart.AxisX>
</lvc:CartesianChart>
</Grid>
</Window>
MainWindow.xaml.cs
using LiveCharts;
using LiveCharts.Wpf;
using System;
using System.Windows;
namespace LiveChartsTest
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// Create date labels
Labels = new string[10];
for (int i = 0; i < Labels.Length; i++)
{
Labels[i] = DateTime.Now.Add(TimeSpan.FromDays(i)).ToString("dd MMM yyyy");
}
Series = new SeriesCollection
{
new LineSeries
{
Title = "Dataset 1",
Values = new ChartValues<double>
{
4,
5,
7,
double.NaN,
double.NaN,
5,
2,
8,
double.NaN,
6
}
},
new LineSeries
{
Title = "Dataset 2",
Values = new ChartValues<double>
{
2,
3,
4,
5,
6,
3,
1,
4,
5,
3
}
}
};
DataContext = this;
}
public SeriesCollection Series { get; set; }
public string[] Labels { get; set; }
}
}
Is there any way to do this with LiveCharts?
That looks more like a math trigonometry problem, where you have to figure out the coordinates of the missing points and add them to SeriesCollection so you can end up with that flat looing joints between the fragments.
Consider the following explanatory picture based on your graph:
Between the X and Y we have to deduce two points, A and B (we know that we need two in between because we can deduce that from the interval between X and Y or we can simply count the NaNs in the initial collection).
A & B Y coordinates could be easily deduced using what we already know and the angle α.
We are looking to calculate the |BB'| and |AA'| sizes (added to the distance between y and the index should represent the final A and B)
We know basically that: tan(α)= |BB'|/|B'Y| = |AA'|/|A'Y| = |XZ|/|ZY|
For simplicity now let's assume that all intervals in the X-axis and Y-axis are equal 1, I will come back to this later.
Now we do know |XZ|/|ZY|, (xz is the difference between x and y, and zy is basically how many NaNs there is in between), so we can easily calculate |BB'| and |AA'|:
|BB'| = (|XZ|/|ZY|) * |B'Y| (Note that |B'Y| is equal to one since it's a one unit interval)
|AA'| = (|XZ|/|ZY|) * |A'Y| (Note that |A'Y| is equal to two-unit interval )
Here how a basic implementation to what was explained above looks like (the code should be self-explanatory):
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// Create date labels
Labels = new string[10];
for (int i = 0; i < Labels.Length; i++)
{
Labels[i] = DateTime.Now.Add(TimeSpan.FromDays(i)).ToString("dd MMM yyyy");
}
var chartValues = new ChartValues<double>
{
4,
5,
7,
double.NaN,
double.NaN,
5,
2,
8,
double.NaN,
6
};
Series = new SeriesCollection
{
new LineSeries
{
Title = "Dataset 1",
Values = ProcessChartValues(chartValues)
},
new LineSeries
{
Title = "Dataset 2",
Values = new ChartValues<double>
{
2,
3,
4,
5,
6,
3,
1,
4,
5,
3
}
}
};
DataContext = this;
}
private ChartValues<double> ProcessChartValues(ChartValues<double> chartValues)
{
var tmpChartValues = new ChartValues<double>();
double bornLeft =0, bornRight=0;
double xz = 0, zy = 0, xy = 0;
bool gapFound = false;
foreach (var point in chartValues)
{
if (!double.IsNaN(point))
{
if (gapFound)
{
// a gap was found and it needs filling
bornRight = point;
xz = Math.Abs(bornLeft - bornRight);
for (double i = zy; i >0; i--)
{
tmpChartValues.Add((xz / zy) * i + Math.Min(bornLeft, bornRight));
}
tmpChartValues.Add(point);
gapFound = false;
zy = 0;
}
else
{
tmpChartValues.Add(point);
bornLeft = point;
}
}
else if(gapFound)
{
zy += 1;
}
else
{
zy += 1;
gapFound = true;
}
}
return tmpChartValues;
}
public SeriesCollection Series { get; set; }
public string[] Labels { get; set; }
}
And here the output:
Now coming back to our interval size, notice how the fragments aren't sharp because of our interval=1 assumption, but on the other hand, this assumption gave the graph some smoothness which is most likely what anyone would be after. If you still need to have sharp fragments, you could explore the LiveChart API to get that interval in pixels which I am not sure they offer (then simply multiply with xz and zy sizes) otherwise you could deduce it from the ActualWidth and ActualHight of the chart drawn area.
As a final note, the code should be extended to handle the NaN points on the edges (you have to either neglect them or define a direction to which the graph should go).

LiveCharts: how to plot skewed data (i.e exponential y-axis) in column series wpf

How to set the Y-axis interval exponentially in column series?
new ColumnSeries
{
Fill = new SolidColorBrush(Color.FromRgb(30,130,173)),
Width = 100,
MaxColumnWidth = 100,
Values = new ChartValues<double> {500,30,10},
DataLabels = true,
LabelPoint = point => point.Y +"",
FontSize = 20
}
You can configure your y-axis with a logarithmic scale - there's an explanation of how to do this on the Live Charts site https://lvcharts.net/App/examples/v1/wpf/Logarithmic%20Scale
Here's an example adapted for a column series:
public SeriesCollection SeriesCollection { get; set; }
public MainWindow()
{
InitializeComponent();
var mapper = Mappers.Xy<double>()
.X((value, index) => index)
.Y((value, index) => Math.Log(value, 10));
SeriesCollection = new SeriesCollection(mapper)
{
new ColumnSeries
{
Values = new ChartValues<double>{500,30,10}
}
};
DataContext = this;
}
and the XAML:
<Grid>
<lvc:CartesianChart Series="{Binding SeriesCollection}">
<lvc:CartesianChart.AxisY>
<lvc:LogarithmicAxis Base="10" />
</lvc:CartesianChart.AxisY>
</lvc:CartesianChart>
</Grid>

How to plot MULTIPLE LineSeries on an OxyPlot chart?

I apologize for asking so many OxyPlot questions, but I seem to be really struggling with using the OxyPlot chart control.
My project is in WPF format so I was originally using a hosted WINFORMS chart and that worked like a charm and did absolutely everything I needed it to until I needed to overlay a WPF element on top of the hosted winform chart. Due to the "AirSpace" issue, I was not able to see the WPF element that I put on top of the hosted chart no matter what I did. That is when I decided to go with OxyPlot, which is giving me quite a few headaches so far.
Here is my origional question! that I asked over at CodePlex. I don't seem to be getting much help over there so I am trying again here.
My question is:
Does anyone know how to plot MULTIPLE LineSeries onto a Plot??
My approach so far:
I am taking a c# List array and adding a new copy of the LineSeries that holds new data to be plotted. My code:
// Function to plot data
private void plotData(double numWeeks, double startingSS)
{
// Initialize new Salt Split class for acess to data variables
Salt_Split_Builder calcSS = new Salt_Split_Builder();
calcSS.compute(numWeeks, startingSS, maxDegSS);
// Create the OxyPlot graph for Salt Split
OxyPlot.Wpf.PlotView plot = new OxyPlot.Wpf.PlotView();
var model = new PlotModel();
// Add Chart Title
model.Title = "Salt Split Degradation";
// Create new Line Series
LineSeries linePoints = new LineSeries() { StrokeThickness = 1, MarkerSize = 1, Title = numWeeks.ToString() + " weeks" };
// Add each point to the new series
foreach (var point in calcSS.saltSplitCurve)
{
DataPoint XYpoint = new DataPoint();
XYpoint = new DataPoint(point.Key, point.Value * 100);
linePoints.Format("%", XYpoint.Y);
linePoints.Points.Add(XYpoint);
}
listPointAray.Add(linePoints);
// Define X-Axis
var Xaxis = new OxyPlot.Axes.LinearAxis();
Xaxis.Maximum = numWeeks;
Xaxis.Minimum = 0;
Xaxis.Position = OxyPlot.Axes.AxisPosition.Bottom;
Xaxis.Title = "Number of Weeks";
model.Axes.Add(Xaxis);
//Define Y-Axis
var Yaxis = new OxyPlot.Axes.LinearAxis();
Yaxis.MajorStep = 15;
Yaxis.Maximum = calcSS.saltSplitCurve.Last().Value * 100;
Yaxis.MaximumPadding = 0;
Yaxis.Minimum = 0;
Yaxis.MinimumPadding = 0;
Yaxis.MinorStep = 5;
Yaxis.Title = "Percent Degradation";
model.Axes.Add(Yaxis);
// Add Each series to the
foreach (var series in listPointAray)
{
LineSeries newpoints = new LineSeries();
newpoints = linePoints;
model.Series.Add(newpoints);
}
// Add the plot to the window
plot.Model = model;
SaltSplitChartGrid.Children.Add(plot);
}
My code works the first time I press my "Graph Data" button, but fails on consecutive attempts with the following error:
The element cannot be added, it already belongs to a Plot Model
The following plot is the type of plot I would like to produce (it worked fine using WinForms Chart control):
I would like a new line with a new color to be plotted each time I run the method.
This is how I've created multi lines on an OxyPlot chart before, the key is creating a set of DataPoints for each series - called circlePoints & linePoints in the following example code, these are then bound to the CircleSeries and LineSeries:
var xAxis = new DateTimeAxis
{
Position = AxisPosition.Bottom,
StringFormat = Constants.MarketData.DisplayDateFormat,
Title = "End of Day",
IntervalLength = 75,
MinorIntervalType = DateTimeIntervalType.Days,
IntervalType = DateTimeIntervalType.Days,
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.None,
};
var yAxis = new LinearAxis
{
Position = AxisPosition.Left,
Title = "Value",
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.None
};
var plot = new PlotModel();
plot.Axes.Add(xAxis);
plot.Axes.Add(yAxis);
var circlePoints = new[]
{
new ScatterPoint(DateTimeAxis.ToDouble(date1), value1),
new ScatterPoint(DateTimeAxis.ToDouble(date2), value2),
};
var circleSeries = new ScatterSeries
{
MarkerSize = 7,
MarkerType = MarkerType.Circle,
ItemsSource = circlePoints
};
var linePoints = new[]
{
new DataPoint(DateTimeAxis.ToDouble(date1), value1),
new DataPoint(DateTimeAxis.ToDouble(date2), value2),
};
var lineSeries = new LineSeries
{
StrokeThickness = 2,
Color = LineDataPointColor,
ItemsSource = linePoints
};
plot.Series.Add(circleSeries);
plot.Series.Add(lineSeries);
Sucess!!!!
AwkwardCoder, thank you for the help, but I realized my mistake was just me having overlooked some things!
Here is the version of the code that works:
// Make a new plotmodel
private PlotModel model = new PlotModel();
// Create the OxyPlot graph for Salt Split
private OxyPlot.Wpf.PlotView plot = new OxyPlot.Wpf.PlotView();
// Function to plot data
private void plotData(double numWeeks, double startingSS)
{
List<LineSeries> listPointAray = new List<LineSeries>();
// Initialize new Salt Split class for acess to data variables
Salt_Split_Builder calcSS = new Salt_Split_Builder();
calcSS.compute(numWeeks, startingSS, maxDegSS);
// Create new Line Series
LineSeries linePoints = new LineSeries()
{ StrokeThickness = 1, MarkerSize = 1, Title = numWeeks.ToString() + " weeks" };
// Add each point to the new series
foreach (var point in calcSS.saltSplitCurve)
{
DataPoint XYpoint = new DataPoint();
XYpoint = new DataPoint(point.Key, point.Value * 100);
linePoints.Format("%", XYpoint.Y);
linePoints.Points.Add(XYpoint);
}
listPointAray.Add(linePoints);
// Add Chart Title
model.Title = "Salt Split Degradation";
// Add Each series to the
foreach (var series in listPointAray)
{
// Define X-Axis
OxyPlot.Axes.LinearAxis Xaxis = new OxyPlot.Axes.LinearAxis();
Xaxis.Maximum = numWeeks;
Xaxis.Minimum = 0;
Xaxis.Position = OxyPlot.Axes.AxisPosition.Bottom;
Xaxis.Title = "Number of Weeks";
model.Axes.Add(Xaxis);
//Define Y-Axis
OxyPlot.Axes.LinearAxis Yaxis = new OxyPlot.Axes.LinearAxis();
Yaxis.MajorStep = 15;
Yaxis.Maximum = calcSS.saltSplitCurve.Last().Value * 100;
Yaxis.MaximumPadding = 0;
Yaxis.Minimum = 0;
Yaxis.MinimumPadding = 0;
Yaxis.MinorStep = 5;
Yaxis.Title = "Percent Degradation";
//Yaxis.StringFormat = "{0.00} %";
model.Axes.Add(Yaxis);
model.Series.Add(series);
}
// Add the plot to the window
plot.Model = model;
plot.InvalidatePlot(true);
SaltSplitChartGrid.Children.Clear();
SaltSplitChartGrid.Children.Add(plot);
}
Here are the multiple things I did wrong:
In my foreach var series loop, I was adding the original series which had already been added and NOT the next var series in the list! (dumb!)
I was creating a new model each time I ran the method. This means that each time the code ran, I was adding a series that already existed in the previous model. (also dumb!)
I was creating a new plot every time and trying to add a model in the new plot that already belonged to a previous plot. (getting dummer..)
The plot was being added to the grid each time I ran the method, so I had to CLEAR the grid's children first before re-adding the same plot.
I was not refreshing the plot.
That was a lot of mistakes, but I worked through it. Hopefully this helps someone in the future. Also, I know I am not using ordinary data binding techniques, but this, at-least, works.
Final result:
Here is how you can achieve a similar result in XAML especially if you are using the MVVM approach.
ViewModel:
public ObservableCollection<DataPoint> DataPointList1 {get;set;}
public ObservableCollection<DataPoint> DataPointList2 {get;set;}
public ObservableCollection<DataPoint> DataPointList3 {get;set;}
public ObservableCollection<DataPoint> DataPointList4 {get;set;}
Using a for loop like below populates DataPointList1 to DataPointList4 with the appropriate datasets.
for (int i = 0; i < dataList.Count; i++)
{
DataPointList1 .Add(new DataPoint{dataList[i].XValue,dataList[i].YValue });
}
XAML:
xmlns:oxy="http://oxyplot.org/wpf"
<oxy:Plot LegendPlacement="Outside" LegendPosition="RightMiddle" Title="Your Chart Title" >
<oxy:Plot.Axes>
<oxy:LinearAxis Title="Your X-axis Title" Position="Bottom" IsZoomEnabled="True" />
<oxy:LinearAxis Title="Your Y-axis Title" Position="Left" IsZoomEnabled="True" />
</oxy:Plot.Axes>
<oxy:Plot.Series>
<oxy:LineSeries Title="Plot1" Color="Black" ItemsSource="{Binding DataPointList1 }"/>
<oxy:LineSeries Title="Plot2" Color="Green" ItemsSource="{Binding DataPointList2 }"/>
<oxy:LineSeries Title="Plot3" Color="Blue" ItemsSource="{Binding DataPointList3 }"/>
<oxy:LineSeries Title="Plot4" Color="Red" ItemsSource="{Binding DataPointList4 }"/>
</oxy:Plot.Series>
</oxy:Plot>

Trying to understand pattern to use with BeginInvoke()

I have two TextBlock’s that I am positioning consecutively on a Canvas. The first case works fine:
TextBlock text1 = new TextBlock();
text1.Text = "Not ";
text1.FontSize = 18;
Canvas.SetTop(text1, 20);
Canvas.SetLeft(text1, 20);
canvas.Children.Add(text1);
TextBlock text2 = new TextBlock();
text2.Text = "bad!";
text2.FontSize = 18;
Canvas.SetTop(text2, 20);
canvas.Dispatcher.BeginInvoke(DispatcherPriority.Background,
new DispatcherOperationCallback(delegate(Object state)
{
Canvas.SetLeft(text2, 20 + text1.ActualWidth);
return null;
}
), null);
canvas.Children.Add(text2);
Result:
However, the second case, which does not use BeginInvoke(), fails:
TextBlock text1 = new TextBlock();
text1.Text = "Not ";
text1.FontSize = 18;
Canvas.SetTop(text1, 20);
Canvas.SetLeft(text1, 20);
canvas.Children.Add(text1);
TextBlock text2 = new TextBlock();
text2.Text = "bad!";
text2.FontSize = 18;
Canvas.SetTop(text2, 20);
Canvas.SetLeft(text2, 20 + text1.ActualWidth); // ActualWidth is zero.
canvas.Children.Add(text2);
Result:
Now, I know that in the second case, the WPF rendering has not happened yet. My question is simply this: What is the preferred pattern to use in such a case where I need to know the actual coordinate values for UI controls which are only available after rendering has taken place?
(e.g. Is the approach, where BeginInvoke() is used, a good solution? Should the entire code be enclosed in a giant BeginInvoke()?)
To answer your question:
Dispatcher.BeginInvoke() queues the operation in the Dispatcher's "pending jobs" queue. This allows it to be able to process the addition of the first UI element, and run the Layout and Render passes before continuing to execute your code.
Therefore, when your code is run, the size of the first TextBlock has already been calculated, and you can get it.
Again, I don't know what you're attempting to do, but creating UI elements in code is usually a sign of a poor design. WPF is not winforms and the WPF ways are completely different from the horrible hacks required to do anything in winforms.
Edit:
This is my approach using a WrapPanel and some RenderTransform:
<Window x:Class="MiscSamples.MovingWords"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MovingWords" Height="300" Width="300">
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Thumb DragDelta="Thumb_DragDelta" Margin="2">
<Thumb.Template>
<ControlTemplate>
<TextBlock Text="{Binding Text}"
FontSize="{Binding FontSize}"
Foreground="{Binding Color}"/>
</ControlTemplate>
</Thumb.Template>
<Thumb.RenderTransform>
<TranslateTransform X="{Binding OffsetX}" Y="{Binding OffsetY}"/>
</Thumb.RenderTransform>
</Thumb>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
Code Behind:
public partial class MovingWords : Window
{
public ObservableCollection<MovingWordModel> Words { get; set; }
public MovingWords()
{
InitializeComponent();
Words = new ObservableCollection<MovingWordModel>
{
new MovingWordModel() {Color = "Black", FontSize = 18, Text = "Hello!!"},
new MovingWordModel() {Color = "Black", FontSize = 18, Text = "This"},
new MovingWordModel() {Color = "Black", FontSize = 18, Text = "is"},
new MovingWordModel() {Color = "Black", FontSize = 18, Text = "the"},
new MovingWordModel() {Color = "Black", FontSize = 18, Text = "Power"},
new MovingWordModel() {Color = "Black", FontSize = 18, Text = "of"},
new MovingWordModel() {Color = "Blue", FontSize = 18, Text = "WPF"},
};
DataContext = Words;
}
private void Thumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
var thumb = sender as Thumb;
if (thumb == null)
return;
var word = thumb.DataContext as MovingWordModel;
if (word == null)
return;
word.OffsetX += e.HorizontalChange;
word.OffsetY += e.VerticalChange;
}
}
Data Model:
public class MovingWordModel:PropertyChangedBase
{
public string Text { get; set; }
public int FontSize { get; set; }
public string Color { get; set; }
private double _offsetX;
public Double OffsetX
{
get { return _offsetX; }
set
{
_offsetX = value;
OnPropertyChanged("OffsetX");
}
}
private double _offsetY;
public double OffsetY
{
get { return _offsetY; }
set
{
_offsetY = value;
OnPropertyChanged("OffsetY");
}
}
}
PropertyChangedBase:
public class PropertyChangedBase:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
You can click and drag the words to move them around.
Notice that the values from the dragging will be stored in the OffsetX and OffsetY properties. The only problem with this approach is that you somewhat lose the Resolution Independence, because the offset values will actually move the words from their default position (which is determined by the WrapPanel, therefore they're subject to change depending on the size of the WrapPanel itself).

Categories

Resources