WPF how to create tab items - c#

The number of tab items are not predetermined. I just want to create new tab items and then add new rectangles inside current items.
I am generating new tab items(bellow is code) but how can I add rectangles in current tab?
var _FloorName = (from fn in db.floors select fn.floorname).ToList();
if (_FloorName.Count > 0)
{
for (int i = 0; i < _FloorName.Count; i++)
{
tabControl1.Items.Add(_FloorName[i]);
}
}

Here is one approach you could take:
Add a Grid (or other container) to each TabItem when creating them
Create a Rectangle, with the brush/dimensions you want
Call tabControl1.SelectedContent, cast it to Grid (or your container type)
Call grid.Children.Add(rectangle)
Here is a complete code sample (using copious code-behind).
MainWindow.xaml:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel Margin="12">
<TabControl Name="tabControl1" Height="250" />
<Button Content="Add Rectangle" Click="Button_Click"
Width="90" Height="25" Margin="5" />
</StackPanel>
</Window>
MainWindow.xaml.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
public class Floor
{
public Floor(string name = null)
{
this.Name = name;
}
public string Name { get; set; }
}
public class FakeDb
{
public IEnumerable<Floor> Floors
{
get
{
return new List<Floor>()
{
new Floor("floor1"),
new Floor("floor2"),
new Floor("floor3"),
};
}
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
InitializeTabControl();
}
private void InitializeTabControl()
{
var db = new FakeDb();
var floorNames = (from fn in db.Floors select fn.Name).ToList();
foreach (string floorName in floorNames)
{
var item = new TabItem()
{
Header = floorName,
Content = new Grid(),
};
tabControl1.Items.Add(item);
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var random = new Random();
var rectangle = new Rectangle()
{
Stroke = Brushes.Black,
Fill = Brushes.SkyBlue,
Width = 50,
Height = 75,
Margin = new Thickness(
left: random.NextDouble() * 300,
top: random.NextDouble() * 150,
right: 0,
bottom: 0),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
};
var grid = (Grid)tabControl1.SelectedContent;
grid.Children.Add(rectangle);
}
}

You should have a look at this article from Josh Smith -
Patterns - WPF Apps With The Model-View-ViewModel Design Pattern
This is one of the best articles explaining MVVM implementation, the sample application developed creates tabs at runtime and uses templates to display the internal contents. If you can go the same way you will have a very stable and expandable application.
Note: Code download available from the MSDN Code Gallery

If this is just adding some control, or draw something on Tab, cause it's not very clear from post, I would personally, strongly recommend to define a Template of TabItem in XAML, as it will save you a lot of "drawing fixes" time. If yuo have a Blender it will become even much easier.
EDIT
If you need some sample with binding and whatever, don't have on my hands now the code, but can provide you with a link to open source project. Have look how TabItems are managed there.
SvnRadar
Regards.

Related

Data binding fails when window constructor takes string as parameter (OxyPlot, WPF)

I need to open a new window while passing a string into its constructor, and this seems to preclude some important data binding in the new window.
My MainWindow consists of a TextBox where the user puts in some text and then clicks a button, opening a new window which makes an OxyPlot displaying that text. This is important because this single user input will eventually be used to open many different windows and make different plots using the same text. Can't make the user provide the string every time they open a new window.
MainWindow with some user input
The new window ("FirstPlotWindow" because there will be more windows implemented later) uses OxyPlot to plot these words at random positions. The new window also has a second TextBox and a second button for adding more words to the plot.
FirstPlotWindow with user's words plotted
I've done my best to make it so that when the user types some words into this second box and clicks the second button, more words will be added to the plot. But it doesn't work.
Here is the XAML code for FirstPlotWindow:
<Window x:Class="WpfApp2.FirstPlotWindow"
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:local="clr-namespace:WpfApp2"
xmlns:oxy="http://oxyplot.org/wpf"
mc:Ignorable="d"
Title="FirstPlotWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="5*"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="userInput2" Margin="0,40,0,0" HorizontalAlignment="Center" VerticalAlignment="Top" Width="100" Height="100"/>
<Button x:Name="changePlotButton" Content="Put new words
into plot" HorizontalAlignment="Center" VerticalAlignment="Center" Click="changePlotButton_Click"/>
<oxy:PlotView x:Name="firstPlotView" Grid.Column="1" Model="{Binding MyModel}"/>
</Grid>
As you can see, I haven't set up the DataContext here because I need the plot model to take the original words ("Hello darkness my old friend") as an argument, as you can see below. Here is the code-behind for the window:
using System.Collections.Generic;
using System.Windows;
namespace WpfApp2
{
public partial class FirstPlotWindow : Window
{
private FirstPlotModel modelInWindow;
public FirstPlotModel ModelInWindow
{
get { return (FirstPlotModel)DataContext; }
set { modelInWindow = value; }
}
public FirstPlotWindow(string inputText)
{
InitializeComponent();
ModelInWindow = new FirstPlotModel(inputText);
DataContext = modelInWindow;
}
private void changePlotButton_Click(object sender, RoutedEventArgs e)
{
List<string> moreWords = new List<string>(userInput2.Text.Split(" "));
ModelInWindow.PopulatePlotWithWords(moreWords);
}
}
}
And here is the code for FirstPlotModel.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using OxyPlot;
using OxyPlot.Annotations;
using OxyPlot.Axes;
namespace WpfApp2
{
public class FirstPlotModel: INotifyPropertyChanged
{
public PlotModel MyModel { get; set; }
Random randomizer;
public event PropertyChangedEventHandler PropertyChanged;
public FirstPlotModel(string inputText)
{
MyModel = new PlotModel { Title = "First Plot" };
MyModel.Axes.Add(new LinearAxis { Position = AxisPosition.Bottom });
MyModel.Axes.Add(new LinearAxis { Position = AxisPosition.Left });
List<string> wordList = new List<string>(inputText.Split(" "));
PopulatePlotWithWords(wordList);
}
public void PopulatePlotWithWords(List<string> wordList)
{
randomizer = new Random();
foreach (string word in wordList)
{
MyModel.Annotations.Add(new TextAnnotation
{
TextPosition = new DataPoint(
randomizer.Next(1, 100),
randomizer.Next(0, 100)),
Text = word
});
}
}
private void OnPropertyChanged([CallerMemberName] String propName = null)
{
if (PropertyChanged != null)
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
}
I know that entering the second set of words and pressing the second button successfully changes the MyModel property in the ModelInWindow object, because when I put a breakpoint after changePlotButton_Click I can see that the words have been added. Since the MyModel property changed, I believe the OxyPlot should update. But the OxyPlot doesn't change.
I designed the code to be as close to this as possible: OxyPlot WPF not working with Button Click
But declaring the DataContext in XAML seems impossible here! Please help!!
I can't reproduce your code so it is hard to tell, but I can't see that you update the plot. This has to be done manually with
MyModel.InvalidatePlot(true);
OxyPlot doesn't support binding like this. Implementing INotifyPropertyChanged and OnPropertyChanged will not make an OxyPlot automatically update. Instead, you must declare a new DataContext every time you click; do this in the C# code, whether or not the binding is initially set up in XAML.

How to resolve synchronisation problem with Helix Toolkit rendering on WPF composite render thread

C#, WPF, Helix Toolkit. I am trying to generate a bitmap from a HelixViewport3D and have encountered a few problems.
The first problem is that I cannot find a way to render off-screen. There are a few references to this online (e.g. here) and as far as I can see it does not have a built-in solution.
As a somewhat sub-optimal workaround I have proceeded to render to the screen where the user can see it, with the intention of creating a bitmap from that rendered image. I now have the problem that an image exported from the content on screen (e.g. using Viewport3DHelper.SaveBitmap) is blank if I call it immediately. I understand that this is because Helix Toolkit is rendering the image in the WPF composite render thread, so there is no image to grab at the time I try to grab it, because it has not yet been rendered.
I am not aware of a 'render complete' event that I can subscribe to. Is there one?
If not, would a workaround perhaps be to use thread priorities in order to make my code lower priority, so that it waits for the rendering to complete before continuing?
<Window x:Class=".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:local="clr-namespace:Test"
xmlns:HelixToolkit="clr-namespace:HelixToolkit.Wpf;assembly=HelixToolkit.Wpf" xmlns:h="http://helix-toolkit.org/wpf"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid>
<h:HelixViewport3D x:Name="helixPlot" Width="450" Height="450"/>
</Grid>
<StackPanel Orientation="Horizontal">
<Button Name ="btnGo" Height="25" Content="Render" VerticalAlignment="Bottom" Click="Go_Click"/>
<Button Name ="btnTemp" Height="25" Content="Write PNG" VerticalAlignment="Bottom" Click="Test_Click"/>
</StackPanel>
</Grid>
</Window>
using System.Collections.Generic;
using System.Windows;
using HelixToolkit.Wpf;
using System.Windows.Media.Media3D;
using System.Windows.Media;
namespace Test
{
class Foo
{
public List<Point3D> points;
public Foo()
{ // constructor creates three arbitrary 3D points
points = new List<Point3D>() { new Point3D(0, 0, 0), new Point3D(1, 0, 0), new Point3D(0, 0, 1) };
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void renderImages()
{
Foo bar = new Foo(); // create object with three 3D points
DrawStuff(bar.points); // plot to helixViewport3D control ('points' = list of 3D points)
helixPlot.CameraController.ZoomExtents();
// This results in a blank image because image not yet rendered...
Viewport3DHelper.SaveBitmap(helixPlot.Viewport, #"E:\test.png", null, 4, BitmapExporter.OutputFormat.Png);
}
private void DrawStuff(List<Point3D> points)
{
Point3DCollection dataList = new Point3DCollection();
PointsVisual3D cloudPoints = new PointsVisual3D { Color = Colors.Red, Size = 5.0f };
foreach (Point3D p in points)
{
dataList.Add(p);
}
cloudPoints.Points = dataList;
// Add geometry to helixPlot. It renders asynchronously in the WPF composite render thread...
helixPlot.Children.Add(cloudPoints);
}
// When this is clicked we render image and (try to) save to file...
private void Go_Click(object sender, RoutedEventArgs e)
{
renderImages();
}
// To demonstrate that the image export is not the problem.
// This works if the image has been rendered already...
private void Test_Click(object sender, RoutedEventArgs e)
{
Viewport3DHelper.SaveBitmap(helixPlot.Viewport, #"E:\test.png", null, 4, BitmapExporter.OutputFormat.Png);
}
}
}
I managed to find a solution thanks to this answer:
Dispatcher.BeginInvoke(new Action(() => DoSomething()), DispatcherPriority.ContextIdle, null);
The Action will be called once the rendering is done.
So in your case you could use it like this:
private void renderImages()
{
Foo bar = new Foo(); // create object with three 3D points
this.DrawStuff(bar.points, SaveHelixPlotAsBitmap /*the action you want to perform once the rendering is done*/);
}
private void DrawStuff(List<Point3D> points, Action renderingCompleted)
{
//Draw everyting you need ...
Dispatcher.BeginInvoke(renderingCompleted, DispatcherPriority.ContextIdle);
}
private void SaveHelixPlotAsBitmap()
{
helixPlot.CameraController.ZoomExtents();
Viewport3DHelper.SaveBitmap(helixPlot.Viewport, #"E:\test.png", null, 4, BitmapExporter.OutputFormat.Png);
}

Programmatically created controls not rendering

As I was required to sort of mask input in a textbox, I decided to construct my own control to handle this.
One of many templates could be "Size {enter size} Colour {enter colour}" which I've broken down to create a series of controls. The custom control that extends StackPanel which I've named CustomTextBox generates the following from the constructor.
// Pseudo
Children = {
Label = { Content = "Size" },
TextBox = { Text = "enter size" },
Label = { Content = "Colour" },
TextBox = { Text = "enter colour" }
// .. and an arbitrary amount of more Labels and TextBoxes in no particular order
}
So far so good. But when I want it to render.. That's where my headache starts.
I've tried to add the controls to the Children property and Measure/Arrange on the parent, itself and all the Children. ActualHeight and ActualWidth do change to something other than 0, but they won't render/display/become visible whatsoever.
I've also tried to use an ItemsControl and add the controls to the ItemsSource property to no avail.
I've tried to predefine sizes on everything, colour the background red and all, but the elusive controls remain to be caught and tied to my screen.
There's got to be a huge "Oooh..." here that I just can't find. I refuse to believe that this can't be done. I mean, it's WPF. WPF is awesome.
Edit Updated to what I currently have that seems most likely to work - still doesn't though.
Whatever I do in the designer shows up, but nothing I do in the CustomTextBox makes any visible difference.
Edit
New headline that fits the problem better.
Also, I've found several examples of programmatically adding controls. Take this article for example. I fail to see the difference between my scenario and theirs, except that theirs work and the buttons are visible.
Update3
The mistake was to assume, that one can simply replace control in visual tree by assigning in codebehind a new control to it's name (specified in xaml)
Updated2
Your mistake was following. If you write
<TextBlock Name="tb" Text="tb"/>
and then in code you will do
tb = new TextBlock() { Text = "Test" };
then you will have a new textblock as a variable, and nothing in xaml will change. You either have to change existing control, or remove old control and add new.
I'm talking about your Headline, Subtext & Description. You don't change them
Updated:
Here is an example of dynamically creating controls by specifying input mask:
MainWindow.xaml
<Window x:Class="WpfApplication35.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525" xmlns:local="clr-namespace:WpfApplication35">
<Grid>
<local:UserControl1 x:Name="myUserControl"/>
</Grid>
</Window>
MainWindow.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
myUserControl.BuildControls("a {enter a} b {enter b1}{enter c2}");
}
}
UserControl1.xaml
<UserControl x:Class="WpfApplication35.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="30" d:DesignWidth="300">
<WrapPanel Name="root" Orientation="Horizontal"/>
</UserControl>
UserControl1.cs
public partial class UserControl1 : UserControl
{
public List<CustomField> Fields = new List<CustomField>();
public UserControl1()
{
InitializeComponent();
}
public UserControl1(string mask)
{
InitializeComponent();
BuildControls(mask);
}
public void BuildControls(string mask)
{
//Parsing Input
var fields = Regex.Split(mask, #"(.*?\}\s)");
foreach (var item in fields)
{
if (item != "")
{
int index = item.IndexOf('{');
string namestring = item.Substring(0, index).Trim();
var field = new CustomField() { Name = namestring };
string valuesstring = item.Substring(index, item.Length - index).Trim();
var values = valuesstring.Split(new char[] { '{', '}' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var val in values)
{
var valuewrapper = new FieldValue() { Value = val };
field.Values.Add(valuewrapper);
}
Fields.Add(field);
}
}
foreach (var field in Fields)
{
var stackPanel = new StackPanel() { Orientation = Orientation.Horizontal };
var label = new Label() { Content = field.Name, Margin = new Thickness(4) };
stackPanel.Children.Add(label);
foreach (var item in field.Values)
{
var tb = new TextBox() { Margin = new Thickness(4), Width = 200 };
tb.SetBinding(TextBox.TextProperty, new Binding() { Path = new PropertyPath("Value"), Source = item, Mode = BindingMode.TwoWay });
stackPanel.Children.Add(tb);
}
root.Children.Add(stackPanel);
}
}
}
public class CustomField
{
public string Name { get; set; }
public List<FieldValue> Values = new List<FieldValue>();
}
public class FieldValue
{
public string Value { get; set; }
}
This way fields and values are gonna be represented by Fields collection in UserControl1. Values of fields are updated as user types something. But only one-way, i.e. user input updates corresponding Value property, but changing Value property at runtime will not affect corresponding textbox. To implement updating from Value to textbox you have to implement INotifyProperty interface
Obsolete
Since you've asked.
There are hundreds of possible implementations, depending on what are you trying to archieve, how do you want validation to be, do you want to use MVVM, do you want to use bindings etc. There are generally 2 approaches : creating usercontrol and creating custom control. First one suits you better I believe.
Create a usercontrol with following xaml:
<Grid Height="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Content="Size: " Grid.Column="0"/>
<TextBox Name="tbSize" Grid.Column="1"/>
<Label Content="Colour:" Grid.Column="2"/>
<TextBox Name="tbColour" Grid.Column="3"/>
</Grid>
In code-behind you can access TextBoxes by their name and do whatever you want to do.
You can use usercontrol in both xaml and codebehind.
In xaml:
Specify alias for namespace of your usercontrol (look at xmlns:local)
<Window x:Class="WpfApplication35.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
xmlns:local="clr-namespace:WpfApplication35">
<Grid>
<local:UserControl1/>
</Grid>
</Window>
In codebehind you can use it like this:
public MainWindow()
{
InitializeComponent();
var myUserControl = new UserControl1();
}
There is a lot to say and these are basic things, so check tutorials and ask questions.
P.S. If you are learning WPF it's mandatory to learn bindings.

How to refresh oxyplot plot when data changes

Oxyplot graphs 13 points which are derived from the 6 user input text boxes. The values in the text boxes are held in public variables in the MainWindow.xaml.cs class. The variables are updated when the user presses enter in the text box. How would I make the refresh button refresh the graph.
private void RefreshButton_Click(object sender, RoutedEventArgs e)
{
//Refresh The Graph
}
I think that this would be done using the
PlotModel.RefreshPlot()
method, but I am not sure how to implement it because of Oxyplot's poor documentation.
I just updated to a new version of OxyPlot via NuGet. I'm using OxyPlot.Wpf v20014.1.277.1 and I think you now need to call InvalidatePlot(bool updateData) on the PlotModel instead of RefreshPlot (which is no longer available). I tested this in my sample code and it worked as expected.
If you want to refresh the plot and update the data collections, you need to pass true to the call:
PlotModel.InvalidatePlot(true)
Give x:Name to OxyPlot instance in XAML:
<oxy:Plot x:Name="Plot1"/>
and on button click handler, refresh like this:
private void RefreshButton_Click(object sender, RoutedEventArgs e)
{
Plot1.RefreshPlot(true);
}
The cleanest way I've found to get "sort of" auto-update is reacting to CollectionChanged on the collection that is LineSeries' ItemsSource.
In ViewModel:
ObservableCollection<DataPoint> Data { get; set; }
= new ObservableCollection<DataPoint>();
public PlotModel PlotModel
{
get { return _plot_model; }
set
{
_plot_model = value;
RaisePropertyChanged(() => PlotModel);
}
}
PlotModel _plot_model;
// Inside constructor:
Data.CollectionChanged += (a, b) => PlotModel.InvalidatePlot(true);
In the current OxyPlot.Wpf (1.0.0-unstable1983) you have two options:
Bind the Series.ItemsSource property from XAML to a collection in your viewmodel and exchange the whole collection, when you need an update. This also allows for concurrent async updates with larger data sets.
Bind the Plot.InvalidateFlag property of type int to your viewmodel and increment whenever you need an update. I haven't tested this approach, though.
The following code illustrates both options (pick one). XAML:
<oxy:Plot InvalidateFlag="{Binding InvalidateFlag}">
<oxy:Plot.Series>
<oxy:LineSeries ItemsSource="{Binding DataSeries}" />
</oxy:Plot.Series>
</oxy:Plot>
Updates on the ViewModel:
private async Task UpdateAsync()
{
// TODO do some heavy computation here
List<DataPoint> data = await ...
// option 1: Trigger INotifyPropertyChanged on the ItemsSource.
// Concurrent access is ok here.
this.DataSeries = data; // switch data sets
// option 2: Update the data in place and trigger via flag
// Only one update at a time.
this.DataSeries.Clear();
data.ForEach(this.DataSeries.Add);
this.InvalidateFlag++;
}
After having the same question with the same issue, it would seem that the only working solution (at least to my point of view) is as followed :
PlotView.InvalidatePlot(true)
Doing so, after updating one or multple Series do refresh your PlotView.
The refresh rate depends on how often, or at which rate your serie(s) is/are updated.
Here is a code snippet (on Xamarin Android but should work anyway) :
PlotView resultsChart = FindViewById<PlotView>(Resource.Id.resultsChart);
PlotModel plotModel = new PlotModel
{
// set here main properties such as the legend, the title, etc. example :
Title = "My Awesome Real-Time Updated Chart",
TitleHorizontalAlignment = TitleHorizontalAlignment.CenteredWithinPlotArea,
LegendTitle = "I am a Legend",
LegendOrientation = LegendOrientation.Horizontal,
LegendPlacement = LegendPlacement.Inside,
LegendPosition = LegendPosition.TopRight
// there are many other properties you can set here
}
// now let's define X and Y axis for the plot model
LinearAxis xAxis = new LinearAxis();
xAxis.Position = AxisPosition.Bottom;
xAxis.Title = "Time (hours)";
LinearAxis yAxis = new LinearAxis();
yAxis.Position = AxisPosition.Left;
yAxis.Title = "Values";
plotModel.Axes.Add(xAxis);
plotModel.Axes.Add(yAxis);
// Finally let's define a LineSerie
LineSeries lineSerie = new LineSeries
{
StrokeThickness = 2,
CanTrackerInterpolatePoints = false,
Title = "Value",
Smooth = false
};
plotModel.Series.Add(lineSerie);
resultsChart.Model = plotModel;
Now, whenever you need to add DataPoints to your LineSerie and to updated automatically the PlotView accordingly, just do as followed :
resultsChart.InvalidatePlot(true);
Doing so will automatically refresh your PlotView.
On a side note, the PlotView will also be updated when an event occurs such as a touch, a pinch to zoom, or any kind of UI-related events.
I hope I could help. I had trouble with this for a very long time.
Exists three alternatives how refresh plot (from OxyPlot documentation):
Change the Model property of the PlotView control
Call Invalidate on the PlotView control
Call Invalidate on the PlotModel
Another two years later... this solution works for me, because I have no oxyplot models and I´m missing some of the named functions from above.
code behind:
public partial class LineChart : UserControl
{
public LineChart()
{
InitializeComponent();
DataContext = this;
myChart.Title = "hier könnte Ihr Text stehen!";
this.Points = new List<DataPoint>();
randomPoints();
}
public IList<DataPoint> Points { get; private set; }
public void randomPoints()
{
Random rd = new Random();
String myText = "";
int anz = rd.Next(30, 60);
for (int i = 0; i < anz; i++)
myText += i + "," + rd.Next(0, 99) + ";";
myText = myText.Substring(0, myText.Length - 1);
String[] splitText = myText.Split(';');
for (int i = 0; i < splitText.Length; i++)
{
String[] tmp = splitText[i].Split(',');
Points.Add(new DataPoint(Double.Parse(tmp[0].Trim()), Double.Parse(tmp[1].Trim())));
}
while (Points.Count > anz)
Points.RemoveAt(0);
myChart.InvalidatePlot(true);
}
}
To update your data don't exchange the whole IList, rather add some new DataPoints to it and remove old ones at position 0.
XAML:
<UserControl x:Class="UxHMI.LineChart"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:UxHMI"
xmlns:oxy="http://oxyplot.org/wpf"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid x:Name="Container" Background="White">
<oxy:Plot x:Name="myChart" Title="{Binding Title}" FontFamily="Bosch Sans Medium" Foreground="#FF0C6596" FontSize="19" Canvas.Left="298" Canvas.Top="32" Background="AliceBlue" Margin="0,0,10,0">
<oxy:Plot.Series>
<oxy:LineSeries x:Name="ls" Background="White" ItemsSource="{Binding Points}" LineStyle="Solid" Color="ForestGreen" MarkerType="None" MarkerSize="5" MarkerFill="Black">
</oxy:LineSeries>
</oxy:Plot.Series>
</oxy:Plot>
<Button x:Name="button" Content="Random" HorizontalAlignment="Left" Margin="0,278,0,0" VerticalAlignment="Top" Width="75" Click="button_Click"/>
</Grid>
important are the x:Name="myChart" and ItemsSource="{Binding Points}"
I hope this is useful for someone out there

Resizing Issues with DataGrid containing wrapping TextBlocks

I have a really disturbing issue with .NET 4.0 DataGrid. I have proportional template columns containing TextBlock with textWrapping enabled.
Problem is, the height of the DataGrid is not correct at load time (it's sized as if the textblock were all wrapped at their maximum.) and does not update their size when resizing. It appears to be a layout issue (MeasureOverride and ArrangeOverride seems to be called when proportional sizes are not resolved, and not called afterwards...) but I haven't been able to solve it.
Here is a simplified code that shows the issue :
MainWindow.xaml
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="700" Width="525">
<StackPanel Width="500">
<Button Content="Add DataGrid" Click="Button_Click" />
<ItemsControl x:Name="itemsControl">
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Control.Margin" Value="5" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</StackPanel>
MainWindow.xaml.cs
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace WpfApplication1
{
public partial class MainWindow : System.Windows.Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
itemsControl.Items.Add(CreateDataGrid());
}
private DataGrid CreateDataGrid()
{
var dataGrid = new DataGrid() { HeadersVisibility = DataGridHeadersVisibility.Column };
dataGrid.MaxWidth = 500;
dataGrid.Background = Brushes.LightSteelBlue;
dataGrid.Columns.Add(GetDataGridTemplateColumn("Label", "Auto"));
dataGrid.Columns.Add(GetDataGridTemplateColumn("Value", "*"));
dataGrid.Items.Add(new Entry() { Label = "Text Example 1", Value = "Some wrapped text" });
dataGrid.Items.Add(new Entry() { Label = "Text Example 2", Value = "Some wrapped text" });
return dataGrid;
}
private DataGridTemplateColumn GetDataGridTemplateColumn(string bindingPath, string columnWidth)
{
DataGridTemplateColumn result = new DataGridTemplateColumn() { Width = (DataGridLength)(converter.ConvertFrom(columnWidth))};
FrameworkElementFactory cellTemplateframeworkElementFactory = new FrameworkElementFactory(typeof(TextBox));
cellTemplateframeworkElementFactory.SetValue(TextBox.NameProperty, "cellContentControl");
cellTemplateframeworkElementFactory.SetValue(TextBox.TextProperty, new Binding(bindingPath));
cellTemplateframeworkElementFactory.SetValue(TextBox.TextWrappingProperty, TextWrapping.Wrap);
result.CellTemplate = new DataTemplate() { VisualTree = cellTemplateframeworkElementFactory };
return result;
}
private static DataGridLengthConverter converter = new DataGridLengthConverter();
}
public class Entry
{
public string Label { get; set; }
public string Value { get; set; }
}
}
I finally came up with a rather dirty solution, but at least it works : I'm manually updating the Height of the grid by computing myself the right value, taking into account Rows height, Grid Padding, Grid border and ColumnHeaderRow height. I need to update it on OnPropertyChanged (for padding and grid border), DataGridColumn.SizeChanged, DataGridColumn.RowUnloaded, ColumnHeaderPresenter.SizeChanged, and a few others.
Only issue is that it works correctly with the default DataGrid ControlTemplate but would not be correct anymore if a template was to change the grid render.
Finally found a solution : setting CanContentScroll to false on the DataGrid fixed the issue.
<Setter Property="ScrollViewer.CanContentScroll" Value="False" />

Categories