The desired flow of my project is:
Button click on UI triggers timer start.
Each timer tick changes the string bound to TextBox text.
UI updates to the new string automatically.
For a long time I thought I had a databinding problem because the UI wasn't updating, but then I found if I triggered the timer start via the view model rather than externally via main window code behind, the UI updated just fine. However this isn't the desired behavior, and I'm trying to understand why that's the case.
Full view (TextBlock Text="{Binding FileName}" is the property being updated):
<Window x:Class="MaterialDesignTest.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"
mc:Ignorable="d"
xmlns:ns="clr-namespace:MaterialDesignTest"
xmlns:local="clr-namespace:MaterialDesignTest"
Title="MainWindow" Height="350" Width="525"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
TextElement.Foreground="{DynamicResource MaterialDesignBody}"
TextElement.FontWeight="Regular"
TextElement.FontSize="13"
TextOptions.TextFormattingMode="Ideal"
TextOptions.TextRenderingMode="Auto"
Background="{DynamicResource MaterialDesignPaper}"
FontFamily="{DynamicResource MaterialDesignFont}">
<materialDesign:DialogHost Identifier="RootDialog" Loaded="DialogHost_Loaded">
<Grid>
<StackPanel>
<Button Width="100" x:Name="SearchRestore" Margin="0 150 0 0" Command="{x:Static materialDesign:DialogHost.OpenDialogCommand}" materialDesign:DialogHost.DialogClosingAttached="SearchRestore_OnDialogClosing"
Content="Restore" Click="SearchRestore_Click">
<Button.CommandParameter>
<StackPanel Margin="10">
<TextBlock Text="Restoring..." HorizontalAlignment="Center" Margin="0 0 0 15"/>
<TextBlock Text="{Binding FileName}" HorizontalAlignment="Center">
<TextBlock.DataContext>
<ns:ProgressViewModel />
</TextBlock.DataContext>
</TextBlock>
<Button Name="CircleButton" Margin="0 50 0 0" Style="{StaticResource MaterialDesignFloatingActionButton}" IsHitTestVisible="False"
materialDesign:ButtonProgressAssist.IsIndicatorVisible="{Binding IsSaving}"
materialDesign:ButtonProgressAssist.Value="{Binding SaveProgressButton}">
<materialDesign:PackIcon Height="24" Width="24" Foreground="White">
<materialDesign:PackIcon.Style>
<Style TargetType="materialDesign:PackIcon" BasedOn="{StaticResource {x:Type materialDesign:PackIcon}}">
<Setter Property="Kind" Value="CloudSync" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsSaveComplete}" Value="True">
<Setter Property="Kind" Value="Check" />
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:0.8" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</materialDesign:PackIcon.Style>
</materialDesign:PackIcon>
</Button>
</StackPanel>
</Button.CommandParameter>
</Button>
</StackPanel>
</Grid>
</materialDesign:DialogHost>
Full view model (commenting out KickOffProgressTimer(); causing the UI to no longer update property FileName):
using MaterialDesignThemes.Wpf;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace MaterialDesignTest
{
public class ProgressViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
System.Windows.Forms.Timer progressTimer;
private string _fileName;
public string FileName
{
get { return _fileName; }
set
{
if (value != _fileName)
{
_fileName = value;
OnPropertyChanged("FileName");
}
}
}
private double _saveProgressButton;
public double SaveProgressButton
{
get { return _saveProgressButton; }
set { this.MutateVerbose(ref _saveProgressButton, value, RaisePropertyChanged()); }
}
private bool _isSaveComplete;
public bool IsSaveComplete
{
get { return _isSaveComplete; }
private set { this.MutateVerbose(ref _isSaveComplete, value, RaisePropertyChanged()); }
}
private bool _isSaving;
public bool IsSaving
{
get { return _isSaving; }
private set { this.MutateVerbose(ref _isSaving, value, RaisePropertyChanged()); }
}
int progress = 0;
int cycles = 0;
public ProgressViewModel()
{
KickOffProgressTimer();
}
public void KickOffProgressTimer()
{
progressTimer = new System.Windows.Forms.Timer();
progressTimer.Tick += new EventHandler(progressTimerTick);
progressTimer.Interval = 140;
progressTimer.Start();
}
private async void progressTimerTick(object sender, EventArgs e)
{
if (progress < 100 && cycles < 2)
{
if (progress == 99)
{
cycles++;
progress = 0;
}
FileName = SelectRandomString();
IsSaveComplete = false;
IsSaving = true;
progress++;
SaveProgressButton = progress;
}
else
{
IsSaveComplete = true;
IsSaving = false;
progressTimer.Stop();
progressTimer.Enabled = false;
SaveProgressButton = 0;
await NonBlockingDelay(1750);
DialogHost.CloseDialogCommand.Execute(null, null);
}
}
async Task NonBlockingDelay(int value)
{
await Task.Delay(value);
}
private Action<PropertyChangedEventArgs> RaisePropertyChanged()
{
return args => PropertyChanged?.Invoke(this, args);
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
static string SelectRandomString()
{
var random = new Random();
var filenames = new List<string>(Directory.GetFiles(#"C:\Windows\System32\"));
int index = random.Next(filenames.Count);
return (filenames[index]);
}
}
}
Logic to kickoff timer in main window code behind:
private void CircleButtonClick(object sender, RoutedEventArgs e)
{
ProgressViewModel pvm = new ProgressViewModel();
CircleButton.DataContext = pvm;
pvm.KickOffProgressTimer();
}
Related
I am a VB.Net programmer and quite new to C#. I am at a point where I'm stuck.
I want to make an app to create a quotation with Word. This Quotation should consist of two Word files.
The Word files are Templates with Bookmarks, so writing to them should be no problem.
I want to have a WPF User Interface where the User can describe the Article and when clicking on a button the two Word files will be created.
I made the WPF User Interface and binded the Textboxes to a cl_Data.cs Class where are Properties such as : Description, FunctionName, etc.
My Problem:
How can i access the Data from the User Interface from my Code Behinde to shift it to the Word files?
The Code:
WPF: How i Bind it on .xaml level
<Window.Resources>
<!-- Binding the Data Class-->
<local:Cl_Data x:Key="Data"
Dealer="Test"
Customer="Tester"
Machine="M***s"
PRJ="123456"
DeliveryTime="6"
Description="Managing different chucks, Saving position data of the linear sensor for chuck clamp unclamp position"
Operation="The operator can select a chuck form the chuck management and save the clamp and unclamp position and reuse this position for next time"
FunctionName="GeneratorAPP"
Requirements="API-Kit"
/>
</Window.Resources>
How i call it on .xaml level (same document) -> This works
<Border BorderBrush="#FFB0F0FF" BorderThickness="1" Height="26">
<TextBox x:Name="Tb_Dealer"
TextWrapping="Wrap" Text="{Binding Dealer, UpdateSourceTrigger=PropertyChanged}" Width="auto" Foreground="#FFB0F0FF" BorderBrush="#00ABADB3" Background="Transparent" TextAlignment="Center" VerticalAlignment="Center" />
</Border>
<Border BorderBrush="#FFB0F0FF" BorderThickness="1" Height="26">
<TextBox x:Name="Tb_Dealer" TextWrapping="Wrap" Text="{Binding Dealer, UpdateSourceTrigger=PropertyChanged}" Width="auto" Foreground="#FFB0F0FF" BorderBrush="#00ABADB3" Background="Transparent" TextAlignment="Center" VerticalAlignment="Center" />
</Border>
So my class cl_Data.cs looks like:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using System.Windows;
namespace QuotationApp.Classes
{
internal class Cl_Data : INotifyPropertyChanged
{
#region Descriptions
private string _Dealer ;
public string Dealer
{
get { return _Dealer; }
set
{ _Dealer = value;
OnPropertyChanged("Dealer");
}
}
private string _Customer;
public string Customer
{
get { return _Customer; }
set
{
_Customer = value;
OnPropertyChanged("Customer");
}
}
private string _Machine;
public string Machine
{
get { return _Machine; }
set
{
_Machine = value;
OnPropertyChanged("Machine");
}
}
private string _PRJ;
public string PRJ
{
get { return _PRJ; }
set
{
_PRJ = value;
OnPropertyChanged(PRJ);
}
}
private string _DeliveryTime;
public string DeliveryTime
{
get { return _DeliveryTime; }
set {
_DeliveryTime = value;
OnPropertyChanged("DeliveryTime");
}
}
private string _Operation;
public string Operation
{
get { return _Operation; }
set {
_Operation = value;
OnPropertyChanged("Operation");
}
}
private string _Description;
public string Description
{
get { return _Description; }
set {
_Description = value;
OnPropertyChanged("Description");
}
}
private string _FunctionName;
public string FunctionName
{
get { return _FunctionName; }
set {
_FunctionName = value;
OnPropertyChanged("FunctionName");
}
}
private string _Requirements;
public string Requirements
{
get { return _Requirements; }
set {
_Requirements = value;
OnPropertyChanged("Requirements");
}
}
#endregion
#region Costs
private double _HardwareCost;
public double HardwareCost
{
get { return _HardwareCost; }
set {
_HardwareCost = value;
_CostTotal = CalcTotal();
OnPropertyChanged("HardwareCost");
}
}
private double _PersonalCost;
public double PersonalCost
{
get { return _PersonalCost; }
set {
_PersonalCost = value;
_CostTotal = CalcTotal();
OnPropertyChanged("PersonalCost");
}
}
private double _TravelCost;
public double TravelCost
{
get { return _TravelCost; }
set {
_TravelCost = value;
_CostTotal = CalcTotal();
OnPropertyChanged("TravelCost");
}
}
private double _CostTotal;
public double CostTotal
{
get { return _CostTotal; }
set {
_CostTotal = value;
OnPropertyChanged("CostTotal");
}
}
public double CalcTotal()
{
double total = 0;
try
{
total = TravelCost + HardwareCost + PersonalCost;
}
catch (Exception e)
{
MessageBox.Show("Error getting the total Value: " + e.Message);
}
return total;
}
#endregion
#region PropertyChangedEvents
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
So now i want to access these Data for example the Description (Data.Description) to process it to a word Bookmark. But how can i Access this Data on WPF level from CodeBehind?
Please be easy with me, i know this question is wierd but i googled 2 days now an i am starting to get frustrated. If this question is answered somewhere else, i would love to have the link to the answer.
Thanks in advance
I made the most simple example as far as it came to my mind.
If you don't understand, ask questions about it.
I will try to answer.
using System;
namespace Core2022.Lexxy_B
{
public class PersonDto
{
public int Id { get; }
public string Name { get; }
public int Age { get; }
public PersonDto(int id, string name, int age)
{
if (Id < 0)
throw new ArgumentOutOfRangeException(nameof(id));
Id = id;
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentNullException(nameof(name));
Name = name;
if (age < 0)
throw new ArgumentOutOfRangeException(nameof(age));
Age = age;
}
public PersonDto(string name, int age)
: this(0, name, age)
{
Id = -1;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
namespace Core2022.Lexxy_B
{
public class PeopleModel
{
private readonly List<PersonDto> people = new List<PersonDto>()
{
new PersonDto(5, "Thomas", 25),
new PersonDto(1, "Harry", 40),
};
public IReadOnlyList<PersonDto> GetPeople() => Array.AsReadOnly(people.ToArray());
public void AddPerson(PersonDto person)
{
int id = people.LastOrDefault()?.Id ?? 0;
do
{
id++;
} while (people.Any(p => p.Id == id));
person = new PersonDto(id, person.Name, person.Age);
people.Add(person);
AddedPerson?.Invoke(this, person);
}
public event EventHandler<PersonDto>? AddedPerson;
}
}
namespace Core2022.Lexxy_B
{
public class PersonVM
{
public string? Name { get; set; }
public int Age { get; set; }
}
}
using Simplified;
using System.Collections.ObjectModel;
namespace Core2022.Lexxy_B
{
public class PeopleViewModel : ViewModelBase
{
private readonly PeopleModel model = new PeopleModel();
private string _mode = "view";
public ObservableCollection<PersonDto> People { get; } = new ObservableCollection<PersonDto>();
public string ViewMode { get => _mode; private set => Set(ref _mode, value); }
public PeopleViewModel()
{
foreach (var person in model.GetPeople())
{
People.Add(person);
}
model.AddedPerson += OnAddedPerson;
}
private void OnAddedPerson(object? sender, PersonDto newPerson)
{
People.Add(newPerson);
}
public RelayCommand AddPersonCommand => GetCommand<PersonVM>(AddPersonExecute, AddPersonCanExecute);
private void AddPersonExecute(PersonVM person)
{
model.AddPerson(new PersonDto(person.Name ?? string.Empty, person.Age));
ViewMode = "view";
}
private bool AddPersonCanExecute(PersonVM person)
{
return !string.IsNullOrWhiteSpace(person.Name) && person.Age >= 0;
}
public RelayCommand ExitAddingPersonCommand => GetCommand(() => ViewMode = "view");
public RelayCommand BeginAddingPersonCommand => GetCommand(() => ViewMode = "add");
}
}
<Window x:Class="Core2022.Lexxy_B.PeopleWindow"
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:Core2022.Lexxy_B"
mc:Ignorable="d"
Title="PeopleWindow" Height="450" Width="800"
DataContext="{DynamicResource vm}">
<Window.Resources>
<local:PeopleViewModel x:Key="vm"/>
</Window.Resources>
<UniformGrid Columns="2">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListBox x:Name="lBox" ItemsSource="{Binding People}" DisplayMemberPath="Name"/>
<Button Grid.Row="1" Content="Go to Add Person" Padding="15 5" Margin="5"
Command="{Binding BeginAddingPersonCommand}">
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding ViewMode}" Value="add">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</Grid>
<ContentControl x:Name="cp">
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Resources>
<DataTemplate x:Key="view.Template">
<local:PersonDetailsUC/>
</DataTemplate>
<DataTemplate x:Key="add.Template">
<local:AddPersonUC/>
</DataTemplate>
</Style.Resources>
<Setter Property="Content" Value="{Binding}"/>
<Setter Property="ContentTemplate" Value="{StaticResource add.Template}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ViewMode}" Value="view">
<Setter Property="Content" Value="{Binding SelectedItem, ElementName=lBox}"/>
<Setter Property="ContentTemplate" Value="{StaticResource view.Template}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</UniformGrid>
</Window>
<UserControl x:Class="Core2022.Lexxy_B.PersonDetailsUC"
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:Core2022.Lexxy_B"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance Type=local:PersonDto}">
<UniformGrid Columns="1">
<TextBlock Text="{Binding Id, StringFormat={}Id: {0}, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<TextBlock Text="{Binding Name, StringFormat={}Name: {0}}" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<TextBlock Text="{Binding Age, StringFormat={}Age: {0}, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" HorizontalAlignment="Center"/>
</UniformGrid>
</UserControl>
<UserControl x:Class="Core2022.Lexxy_B.AddPersonUC"
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:Core2022.Lexxy_B"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<d:UserControl.DataContext>
<local:PeopleViewModel/>
</d:UserControl.DataContext>
<UserControl.Resources>
<local:PersonVM x:Key="person"/>
</UserControl.Resources>
<UniformGrid Columns="2">
<TextBlock Text="Name" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<TextBox Text="{Binding Name, Source={StaticResource person}}" VerticalAlignment="Center" Margin="10"/>
<TextBlock Text="Age" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<TextBox Text="{Binding Age, Source={StaticResource person}}" VerticalAlignment="Center" Margin="10"/>
<Button Content="Add" Padding="15 5" VerticalAlignment="Center" HorizontalAlignment="Center"
Command="{Binding AddPersonCommand}"
CommandParameter="{Binding Mode=OneWay, Source={StaticResource person}}"/>
<Button Content="Exit" Padding="15 5" VerticalAlignment="Center" HorizontalAlignment="Center"
Command="{Binding ExitAddingPersonCommand}"/>
</UniformGrid>
</UserControl>
Addition due to the Repository.
change Line 33 of Word.cs as i was unable to assign a relative path to open to word template
An example of the implementation of obtaining the full name of files by their path given by a relatively executable assembly.
The file itself in the Project resources must have the properties "Content" - "Copy ...". I have a Russified Studio, so the screenshot is in Russian.
The "bin" folder must not be included in the Project, otherwise, all its contents will also be included in the Assembly.
And here is the code for converting a relative path to an absolute one:
public static readonly string ApplicationFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
public const string TestRelativeNameFile = #"Resources\Word\Test.docx";
public static readonly string TestFullNameFile = Path.Combine(ApplicationFolder, TestRelativeNameFile);
public static void GenerateLocalSolution()
{
try
{
WordApp = new Microsoft.Office.Interop.Word.Application();
TestApp = WordApp.Documents.Open(TestFullNameFile);
}
i bind the DataContext in MainWindow.cs (CodeBehind) to a Class and create a specific Instance of the Object which i access from every other class
This implementation doesn't play well with WPF and OOP.
For example, in Designer mode (when you edit XAML in Studio), you have an empty value in the DataContext. And because of this, you can't use the Binding Builder in development.
If you have an instance of the Cl_Data class specific to each MainWindow instance, then it should be initialized in the Window's XAML:
<Window.Resources>
<classes:Cl_Data
xmlns:classes="clr-namespace:QuotationApp.Classes"
x:Key="data"/>
</Window.Resources>
If the Cl_Data instance is the only one in the application sense and the entire session exists, then you can create it in the App resources:
<Application.Resources>
<classes:Cl_Data
xmlns:classes="clr-namespace:QuotationApp.Classes"
x:Key="data"/>
</Application.Resources>
The window gets it in the DataContext in XAML too.
If its value is needed in the Code Behind, then it must be retrieved either by key or from the DataContext.
<Window x:Class="QuoteApp_EldHasp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
------------------
Title="MainWindow" Height="900" Width="1800"
DataContext="{DynamicResource data}">
// Creating such a public property is a bad idea. But for now, I'm not getting involved.
public /* static */ Cl_Data Data { get; } //= new Cl_Data();
public MainWindow()
{
InitializeComponent();
Data = (Cl_Data)DataContext;
// Or
Data = (Cl_Data)FindResource("data");
}
Next note.
In the Word.GenerateLocalSolution() method, the line "descRange.Text = MainWindow.Data.Dealer" is a VERY STRONG OOP violation. The Word class contains logic for working with data (Domain Logic) - this is its "single-responsibility". And it should not work with UI elements. And "MainWindow.Data" is the View property!
There are several options for correct implementation.
One of them is to get the desired value in the parameter and let the one who called this method decide where to get it from.
public static void GenerateLocalSolution(string text)
{
// Some Code
descRange.Text = text; // MainWindow.Data.Dealer;
private void GenerateButton_Click(object sender, RoutedEventArgs e)
{
var data = (Cl_Data)DataContext;
Word.GenerateLocalSolution(data.Dealer);
}
You should also replace clickers with commands. This will significantly improve the architecture of the application and make the code easier.
I can add texts to the DataGrid and by clicking the button (edit) I can drag the data from the DataGrid back into the text boxes, but my problem is if I change the data after dragging it over, it is not updated in the Datagrid.
<Canvas x:Name="CV_Projekte" Grid.Column="1" Background="White" Visibility="Visible">
<Label Content="Projektnumber:" Canvas.Left="44" Canvas.Top="84" FontSize="14" FontWeight="Bold"/>
<Label Content="Name:" Canvas.Left="43" Canvas.Top="113" FontSize="14" FontWeight="Bold" />
<Label Content="Unterposition:" Canvas.Left="44" Canvas.Top="146" FontSize="14" FontWeight="Bold" />
<Label Content="Describe:" Canvas.Left="44" Canvas.Top="182" FontSize="14" FontWeight="Bold" />
<TextBox Canvas.Left="183" Canvas.Top="89" TextWrapping="Wrap" Width="120" Text="{Binding ProjectNumber}" />
<TextBox Canvas.Left="44" Canvas.Top="216" TextWrapping="Wrap" Width="273" Height="155"/>
<TextBox Canvas.Left="183" Canvas.Top="151" TextWrapping="Wrap" Width="120"/>
<TextBox Canvas.Left="183" Canvas.Top="118" TextWrapping="Wrap" Width="120" Text="{Binding ProjectName}"/>
<Button Content="Add" Canvas.Left="383" Canvas.Top="167" Width="75" Command="{Binding StartCommand}" Opacity="0.2"/>
<Button Content="Edit" Canvas.Left="383" Canvas.Top="201" Width="75" Opacity="0.2" Command="{Binding UpdateCommand}" />
<DataGrid Height="575" Canvas.Left="616" Width="303" ItemsSource="{Binding CareList}" SelectedItem="{Binding SelectedCare}" >
<DataGrid.Columns>
<DataGridTextColumn Header="Number" Binding="{Binding Projektnummer}"/>
<DataGridTextColumn Header="Projektname" Width="184" Binding="{Binding Name}"/>
<DataGridTextColumn Header="Delete"/>
</DataGrid.Columns>
</DataGrid>
</Canvas>
This is the CareViewModel.cs
public class CareViewModel :INotifyPropertyChanged
{
private int _ProtjectNumber;
private string _ProjectName;
private Visibility _TrackingVisibility;
private Visibility _CarreVisibility;
private Care _selectedCare;
public int ProjectNumber
{
get
{
return _ProtjectNumber;
}
set
{
_ProtjectNumber = value;
OnPropertyChanged(nameof(ProjectNumber));
}
}
public string ProjectName
{
get
{
return _ProjectName;
}
set
{
_ProjectName = value;
OnPropertyChanged(nameof(ProjectName));
}
}
public Visibility TrackingVisibility
{
get
{
return _TrackingVisibility;
}
set
{
_TrackingVisibility = value;
OnPropertyChanged(nameof(_TrackingVisibility));
}
}
public Visibility CareVisibility
{
get
{
return _CarreVisibility;
}
set
{
_CarreVisibility = value;
OnPropertyChanged(nameof(CareVisibility));
}
}
public void TrackingView(RoutedEventArgs e)
{
TrackingVisibility = Visibility.Visible;
CareVisibility = Visibility.Collapsed;
}
public void CareView(RoutedEventArgs e)
{
TrackingVisibility = Visibility.Collapsed;
CareVisibility = Visibility.Visible;
}
public ObservableCollection<Care> CareList { get; set; }
public CareViewModel()
{
CareList = new ObservableCollection<Care>(); //List
StartCommand = new OurCommand<RoutedEventArgs>(Setting);
TrackingCommand = new OurCommand<RoutedEventArgs>(TrackingView);
CareCommand = new OurCommand<RoutedEventArgs>(CareView);
UpdateCommand = new OurCommand<RoutedEventArgs>(Bearbeitung);
TrackingVisibility = Visibility.Visible;
CareVisibility = Visibility.Collapsed;
}
public ICommand StartCommand { get; set; }
public ICommand TrackingCommand { get; set; }
public ICommand CareCommand { get; set; }
public ICommand UpdateCommand { get; set; }
public Care SelectedCare
{
get
{
return _selectedCare;
}
set
{
_selectedCare = value;
OnPropertyChanged(nameof(SelectedCare));
}
}
public void Setting(RoutedEventArgs e) // Add the Values to Datagrid
{
var projects = new Care();
projects.Projektnummer = _ProtjectNumber;
projects.Name = _ProjectName;
ProjectNumber = 0;
ProjectName = null;
SelectedCare = projects;
CareList.Add(projects);
}
public void Bearbeitung(RoutedEventArgs e) //Editing the value button
{
ProjectNumber = SelectedCare.Projektnummer;
ProjectName = SelectedCare.Name;
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Class Care
public class Care
{
private int _Projektnummer;
private string _Name;
public int Projektnummer
{
get { return _Projektnummer;}
set { _Projektnummer = value;}
}
public string Name
{
get { return _Name;}
set { _Name = value;}
}
}
Thanks to everyone who wants to help me.
I've created some demo project from source you provided.
Selection of the row fills TextBoxes automatically
Add Button creates new row and make it able to edit
Save button applies changes in TextBoxes
X button deletes the row
0. MVVM Helpers
I don't know what is OurCommand but I'm using almost unchanged this one.
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
public void Execute(object parameter) => _execute(parameter);
}
It supports command parameters and everything else related to ICommand.
To be able not to implement INotifyPropertyChanged in every View Model, I moved the implementation to the base class.
public class NotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
[CallerMemberName] is a compiler trick that allows to call OnPropertyChanged() without arguments. Compiler will fill it automatically with Caller Member Name :) - Property name.
1. Markup
Use Grid and relative positioning of controls. It will help you to make responsive design where layout looks fine for almost any area size.
<Window x:Class="WpfApp1.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:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Width="1200" Height="600">
<Window.DataContext>
<local:CareViewModel/>
</Window.DataContext>
<Window.Resources>
<local:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.5*"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.Resources>
<Style TargetType="TextBlock">
<Setter Property="MinHeight" Value="20"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="Bold"/>
</Style>
<Style TargetType="TextBox">
<Setter Property="MinHeight" Value="20"/>
<Setter Property="Margin" Value="5"/>
<Style.Triggers>
<DataTrigger Binding="{Binding EditingCare}" Value="{x:Null}">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style TargetType="Button">
<Setter Property="Width" Value="75"/>
<Setter Property="Margin" Value="5"/>
</Style>
</Grid.Resources>
<StackPanel>
<TextBlock Text="Projektnumber:"/>
<TextBlock Text="Name:"/>
<TextBlock Text="Unterposition:"/>
<TextBlock Text="Describe:"/>
</StackPanel>
<StackPanel Grid.Column="1">
<TextBox Text="{Binding EditingCare.ProjectNumber}"/>
<TextBox Text="{Binding EditingCare.Name}"/>
<TextBox Text="{Binding EditingCare.Subtitle}"/>
</StackPanel>
<TextBox TextWrapping="Wrap" Grid.Row="1" Grid.ColumnSpan="2" Text="{Binding EditingCare.Description}"/>
<DockPanel Margin="5" HorizontalAlignment="Right" Grid.Row="2" Grid.ColumnSpan="2">
<Button Content="Add" Command="{Binding AddCommand}" />
<Button Content="Save" Command="{Binding UpdateCommand}" />
</DockPanel>
</Grid>
<DataGrid Grid.Column="1" ItemsSource="{Binding CareList}" SelectedItem="{Binding SelectedCare}" AutoGenerateColumns="False" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Number" Binding="{Binding ProjectNumber}"/>
<DataGridTextColumn Header="Projektname" Binding="{Binding Name}"/>
<DataGridTextColumn Header="Unterposition" Binding="{Binding Subtitle}" Width="*"/>
<DataGridTemplateColumn Header="Delete">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="X" Command="{Binding DataContext.DeleteCommand,RelativeSource={RelativeSource AncestorType=DataGrid}}" CommandParameter="{Binding}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
2. Data
To fix the initial problem I've implemented INotifyPropertyChanged for the Care class.
public class Care : NotifyPropertyChanged
{
private int _projectNumber;
private string _name;
private string _description;
private string _subtitle;
public int ProjectNumber
{
get => _projectNumber;
set
{
_projectNumber = value;
OnPropertyChanged();
}
}
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged();
}
}
public string Subtitle
{
get => _subtitle;
set
{
_subtitle = value;
OnPropertyChanged();
}
}
public string Description
{
get => _description;
set
{
_description = value;
OnPropertyChanged();
}
}
public Care() { }
public Care(Care care)
{
ProjectNumber = care.ProjectNumber;
Name = care.Name;
Subtitle = care.Subtitle;
Description = care.Description;
}
}
2. View Model
I played a lot with code and came up with following one.
public class CareViewModel : NotifyPropertyChanged
{
private bool _trackingVisible;
private bool _careVisible;
private Care _selectedCare;
private Care _editingCare;
private ObservableCollection<Care> _careList;
private ICommand _addCommand;
private ICommand _trackingCommand;
private ICommand _careCommand;
private ICommand _updateCommand;
private ICommand _deleteCommand;
public ObservableCollection<Care> CareList
{
get => _careList;
set
{
_careList = value;
OnPropertyChanged();
}
}
public bool TrackingVisibility
{
get => _trackingVisible;
set
{
_trackingVisible = value;
OnPropertyChanged();
}
}
public bool CareVisibility
{
get => _careVisible;
set
{
_careVisible = value;
OnPropertyChanged();
}
}
public Care SelectedCare
{
get => _selectedCare;
set
{
_selectedCare = value;
EditingCare = value is Care care ? new Care(care) : value;
OnPropertyChanged();
}
}
public Care EditingCare
{
get => _editingCare;
set
{
_editingCare = value;
OnPropertyChanged();
}
}
public ICommand TrackingCommand => _trackingCommand ?? (_trackingCommand = new RelayCommand(parameter =>
{
TrackingVisibility = true;
CareVisibility = false;
}));
public ICommand CareCommand => _careCommand ?? (_careCommand = new RelayCommand(parameter =>
{
TrackingVisibility = false;
CareVisibility = true;
}));
// Add the Values to Datagrid
public ICommand AddCommand => _addCommand ?? (_addCommand = new RelayCommand(parameter =>
{
Care newItem = new Care();
CareList.Add(newItem);
SelectedCare = newItem;
}));
//Editing the value button
public ICommand UpdateCommand => _updateCommand ?? (_updateCommand = new RelayCommand(parameter =>
{
int index = CareList.IndexOf(SelectedCare);
CareList[index] = EditingCare;
SelectedCare = CareList[index];
}, parameter => EditingCare != null && SelectedCare != null));
public ICommand DeleteCommand => _deleteCommand ?? (_deleteCommand = new RelayCommand(parameter =>
{
if (parameter is Care care)
{
CareList.Remove(care);
}
}));
public CareViewModel()
{
CareList = new ObservableCollection<Care>();
TrackingVisibility = true;
CareVisibility = false;
}
}
3. Visibility
Among other changes pay attention to former Visibility properties now they are bool. And to attach it to controls here's converter that is already meintoined in Markup.
MVVM doesn't suggest to use UI-related types in View Model or Model unless you need it for special purpose.
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value is bool v && v ? Visibility.Visible : Visibility.Collapsed;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> null;
}
Usage of converter
<Button Visibility="{Binding TrackingVisibility, Converter={StaticResource BoolToVisibilityConverter}}"/>
I am using Material Design in XAML Toolkit. I have the main window with drawer, which contains the list of user controls (app tabs). When I click on them - application tab switches between this controls. I want to add a button to the window, and when I click on it I want to switch between tabs too. You can see important parts of my code here:
<materialDesign:DialogHost Identifier="RootDialog" SnackbarMessageQueue="{Binding ElementName=MainSnackbar, Path=MessageQueue}">
<materialDesign:DrawerHost IsLeftDrawerOpen="{Binding ElementName=MenuToggleButton, Path=IsChecked}">
<materialDesign:DrawerHost.LeftDrawerContent>
<DockPanel MinWidth="212">
<ToggleButton Style="{StaticResource MaterialDesignHamburgerToggleButton}"
DockPanel.Dock="Top"
HorizontalAlignment="Right" Margin="16"
IsChecked="{Binding ElementName=MenuToggleButton, Path=IsChecked, Mode=TwoWay}" />
<ListBox x:Name="DemoItemsListBox" Margin="0 16 0 16" SelectedIndex="0"
ItemsSource="{Binding DemoItems}"
PreviewMouseLeftButtonUp="UIElement_OnPreviewMouseLeftButtonUp">
<ListBox.ItemTemplate>
<DataTemplate DataType="helpers:DemoItem">
<TextBlock Text="{Binding Name}" Margin="32 0 32 0" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</materialDesign:DrawerHost.LeftDrawerContent>
<DockPanel>
<materialDesign:ColorZone Padding="16" materialDesign:ShadowAssist.ShadowDepth="Depth2"
Mode="PrimaryDark" DockPanel.Dock="Top">
<DockPanel>
<ToggleButton Style="{StaticResource MaterialDesignHamburgerToggleButton}" IsChecked="False"
x:Name="MenuToggleButton"/>
<materialDesign:PopupBox x:Name="popupBox">
<TextBlock>Check me please</TextBlock>
</materialDesign:PopupBox>
<CheckBox x:Name="LizenzeCheckBox" DockPanel.Dock="Right" Style="{StaticResource MaterialDesignCheckBox}" Tag="False">
<CheckBox.IsChecked>
<Binding Path="Tag" RelativeSource="{RelativeSource Self}">
<Binding.ValidationRules>
<helpers:IsCheckedValidationRule />
</Binding.ValidationRules>
</Binding>
</CheckBox.IsChecked>CheckBox text</CheckBox>
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="22">My App</TextBlock>
</DockPanel>
</materialDesign:ColorZone>
<Button x:Name="TheBUTTON" Click="Button_Click">Ckicc</Button>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ScrollViewer Grid.Row="1"
HorizontalScrollBarVisibility="{Binding ElementName=DemoItemsListBox, Path=SelectedItem.HorizontalScrollBarVisibilityRequirement}"
VerticalScrollBarVisibility="{Binding ElementName=DemoItemsListBox, Path=SelectedItem.VerticalScrollBarVisibilityRequirement}"
Padding="{Binding ElementName=DemoItemsListBox, Path=SelectedItem.MarginRequirement}">
<ContentControl Content="{Binding ElementName=DemoItemsListBox, Path=SelectedItem.Content}" />
</ScrollViewer></Grid>
This is my main window xaml code, as you can see, I bind ListBox Values to DemoItem[] array from viewModel. "TheButton" onclick event is the event which I want to use for tab switching.
My main window view model is:
public class MainWindowViewModel
{
public DemoItem[] DemoItems { get; }
public MainWindowViewModel(ISnackbarMessageQueue snackbarMessageQueue)
{
if (snackbarMessageQueue == null) throw new ArgumentNullException(nameof(snackbarMessageQueue));
DemoItems = new[]
{
new DemoItem("Tab1", new Tab1()),
new DemoItem("Tab2", new Tab2()),
new DemoItem("Tab3", new Tab3()),
};
}
}
The MainWindow.cs is:
public partial class MainWindow : Window
{
public static Snackbar Snackbar;
public MainWindow()
{
InitializeComponent();
Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
}).ContinueWith(t =>
{
MainSnackbar.MessageQueue.Enqueue("Welcome to my app");
}, TaskScheduler.FromCurrentSynchronizationContext());
DataContext = new MainWindowViewModel(MainSnackbar.MessageQueue);
}
private void UIElement_OnPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
//until we had a StaysOpen glag to Drawer, this will help with scroll bars
var dependencyObject = Mouse.Captured as DependencyObject;
while (dependencyObject != null)
{
if (dependencyObject is ScrollBar) return;
dependencyObject = VisualTreeHelper.GetParent(dependencyObject);
}
MenuToggleButton.IsChecked = false;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
//what to do here?
}
}
The DemoItem Class is:
public class DemoItem : INotifyPropertyChanged
{
private string _name;
private object _content;
private ScrollBarVisibility _horizontalScrollBarVisibilityRequirement;
private ScrollBarVisibility _verticalScrollBarVisibilityRequirement;
private Thickness _marginRequirement = new Thickness(16);
public DemoItem(string name, object content)
{
_name = name;
Content = content;
}
public string Name
{
get { return _name; }
set { this.MutateVerbose(ref _name, value, RaisePropertyChanged()); }
}
public object Content
{
get { return _content; }
set { this.MutateVerbose(ref _content, value, RaisePropertyChanged()); }
}
public ScrollBarVisibility HorizontalScrollBarVisibilityRequirement
{
get { return _horizontalScrollBarVisibilityRequirement; }
set { this.MutateVerbose(ref _horizontalScrollBarVisibilityRequirement, value, RaisePropertyChanged()); }
}
public ScrollBarVisibility VerticalScrollBarVisibilityRequirement
{
get { return _verticalScrollBarVisibilityRequirement; }
set { this.MutateVerbose(ref _verticalScrollBarVisibilityRequirement, value, RaisePropertyChanged()); }
}
public Thickness MarginRequirement
{
get { return _marginRequirement; }
set { this.MutateVerbose(ref _marginRequirement, value, RaisePropertyChanged()); }
}
public event PropertyChangedEventHandler PropertyChanged;
private Action<PropertyChangedEventArgs> RaisePropertyChanged()
{
return args => PropertyChanged?.Invoke(this, args);
}
}
My MutateVerbose function looks like:
public static void MutateVerbose<TField>(this INotifyPropertyChanged instance, ref TField field, TField newValue, Action<PropertyChangedEventArgs> raise, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<TField>.Default.Equals(field, newValue)) return;
field = newValue;
raise?.Invoke(new PropertyChangedEventArgs(propertyName));
}
I don't know how to switch tabs with button click in this situation. Help me, please!
I'm trying to start a storyboard animation from my viewmodel. The idea is that there is an animation every time my application receives a position update from a server later on, but for now it would be fine to have a proof of concept where I can update the position and start the animation by pressing a button.
My first approach was to use Microsoft.Expression.Interactivity ControlStoryboardAction (Feel free to tell me if there is a better approach for what I want to achieve). I'm binding the trigger to the CanAnimate Property, which is set to true by default, so the animation should start right after loading - but it doesn't.
The storyboard works when I use an Eventtrigger, but this is not what I want:
<Window x:Class="AnimationExample.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:AnimationExample"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Canvas Name="CanvasWrapper" Width="525" Height="350">
<Ellipse Fill="Red" Width="60" Height="60" Canvas.Left="60" Canvas.Top="60">
<Ellipse.Resources>
<Storyboard x:Key="Movement">
<DoubleAnimation
Storyboard.TargetProperty="(Canvas.Left)"
Duration="0:0:0.1" To="{Binding NextPosX}" />
<DoubleAnimation
Storyboard.TargetProperty="(Canvas.Top)"
Duration="0:0:0.1" To="{Binding NextPosY}" />
</Storyboard>
</Ellipse.Resources>
<!-- This trigger is just for testing and works fine! -->
<Ellipse.Triggers>
<EventTrigger RoutedEvent="Ellipse.MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard Storyboard="{StaticResource Movement}"/>
</EventTrigger.Actions>
</EventTrigger>
</Ellipse.Triggers>
<!-- This trigger doesn't works -->
<i:Interaction.Triggers>
<ei:DataTrigger Binding="{Binding CanAnimate}" Value="True" Comparison="Equal">
<ei:ControlStoryboardAction Storyboard="{StaticResource Movement}" ControlStoryboardOption="Play" />
</ei:DataTrigger>
</i:Interaction.Triggers>
</Ellipse>
</Canvas>
</Window>
This is my viewmodel so far. Obviously the Binding to the ActionCommand is missing, and the ActionCommand should also change the CanAnimate so that the Animation is triggered again, but it should still animate one time due to CanAnimate being true:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace AnimationExample
{
class MainWindowViewModel : INotifyPropertyChanged
{
int _nextPosX = 120;
public int NextPosX
{
get { return _nextPosX; }
set
{
if (value != _nextPosX)
{
_nextPosX = value;
OnPropertyChanged("NextPosX");
}
}
}
int _nextPosY = 120;
public int NextPosY
{
get { return _nextPosY; }
set
{
if (value != _nextPosY)
{
_nextPosY = value;
OnPropertyChanged("NextPosY");
}
}
}
bool _canAnimate = true;
public bool CanAnimate
{
get { return _canAnimate; }
set
{
if (value != _canAnimate)
{
_canAnimate = value;
OnPropertyChanged("CanAnimate");
}
}
}
ICommand _calculate;
public ICommand CalculateNextPos
{
get
{
if(_calculate == null)
{
_calculate = new ActionCommand(dummy =>
{
NextPosX = 500;
NextPosY = 500;
}, dummy => true);
}
return _calculate;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
Thanks in advance!
I am using WPF Toolkit for charting. I am using a LineSeries for displaying the data change per second. Currently, I am able to update the graph as new points are added. But the X-Axis scale is fixed from 0 to 60 automatically. What I want is, after the first cycle, instead of the data plot showing from the starting of the axis, I want the X-Axis to shift by one division, like it is in an ECG display.
I managed to find the answer on my own. Please give suggestions fro improving the answer.
namespace WpfChartExample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
ObservableCollection<ChartData> chartData;
ChartData objChartData;
Thread MyThread;
public MainWindow()
{
InitializeComponent();
chartData = new ObservableCollection<ChartData>();
DateTime dtnow = DateTime.Now;
objChartData = new ChartData() { Name = dtnow, Value = 0.0 };
chartData.Add(objChartData);
chartData.Add(new ChartData() { Name = (dtnow + TimeSpan.FromSeconds(2)), Value = new Random().NextDouble() * 100 });
chartData.Add(new ChartData() { Name = (dtnow + TimeSpan.FromSeconds(4)), Value = new Random().NextDouble() * 100 });
xAxis.Minimum = chartData[0].Name;
simChart.DataContext = chartData;
MyThread = new Thread(new ThreadStart(StartChartDataSimulation));
}
public void StartChartDataSimulation()
{
int i = 0;
while (true)
{
Dispatcher.Invoke(new Action(() =>
{
var data = new ChartData() { Name = DateTime.Now, Value = new Random().NextDouble() * 100 };
chartData.Add(data);
if (chartData.Count % 40 == 0 && i == 0)
{
xAxis.Minimum = chartData[i + 1].Name;
i++;
}
if (i >= 1)
{
xAxis.Minimum = chartData[i + 1].Name;
i++;
}
}));
Thread.Sleep(1000);
}
}
private void btnStartStop_Click(object sender, RoutedEventArgs e)
{
if ((string)btnStartStop.Content == "Start Simulation")
{
if (MyThread.ThreadState == ThreadState.Unstarted)
{
MyThread.Start();
}
else if (MyThread.ThreadState == ThreadState.Suspended)
{
MyThread.Resume();
}
btnStartStop.Content = "Stop Simulation";
}
else
{
MyThread.Suspend();
btnStartStop.Content = "Start Simulation";
}
}
private void Window_Closing(object sender, CancelEventArgs e)
{
if (MyThread.ThreadState == ThreadState.Running ||MyThread.ThreadState == ThreadState.WaitSleepJoin)
{
MyThread.Suspend();
}
Application.Current.Shutdown();
}
}
public class ChartData : INotifyPropertyChanged
{
DateTime _Name;
double _Value;
#region properties
public DateTime Name
{
get
{
return _Name;
}
set
{
_Name = value;
OnPropertyChanged("Name");
}
}
public double Value
{
get
{
return _Value;
}
set
{
_Value = value;
OnPropertyChanged("Value");
}
}
#endregion
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
<Window x:Class="WpfChartExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:chrt="clr-namespace:System.Windows.Controls.DataVisualization.Charting;assembly=System.Windows.Controls.DataVisualization.Toolkit"
Title="MainWindow" Height="350" Width="525" Closing="Window_Closing">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<chrt:Chart x:Name="simChart" Title="Simulation" Background="Beige">
<chrt:LineSeries IndependentValueBinding="{Binding Name}"
DependentValueBinding="{Binding Value}"
ItemsSource="{Binding}"
Background="DarkGray">
<chrt:LineSeries.DataPointStyle>
<Style TargetType="{x:Type chrt:LineDataPoint}">
<Setter Property="BorderBrush" Value="Red" />
<Setter Property="Width" Value="5" />
<Setter Property="Height" Value="5" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="chrt:LineDataPoint">
<Grid x:Name="Root" Opacity="1">
<ToolTipService.ToolTip>
<StackPanel Margin="2,2,2,2">
<ContentControl Content="{TemplateBinding IndependentValue}"
ContentStringFormat="X-Value: {0:HH:mm:ss}"/>
<ContentControl Content="{TemplateBinding DependentValue}"
ContentStringFormat="Y-Value: {0:###.###}"/>
</StackPanel>
</ToolTipService.ToolTip>
<Ellipse StrokeThickness="{TemplateBinding BorderThickness}"
Stroke="{TemplateBinding BorderBrush}"
Fill="{TemplateBinding Background}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</chrt:LineSeries.DataPointStyle>
<chrt:LineSeries.IndependentAxis>
<chrt:DateTimeAxis Name="xAxis" ShowGridLines="True" Orientation="X">
<chrt:DateTimeAxis.AxisLabelStyle>
<Style TargetType="chrt:DateTimeAxisLabel">
<Setter Property="StringFormat" Value="{}{0:mm:ss}" />
</Style>
</chrt:DateTimeAxis.AxisLabelStyle>
</chrt:DateTimeAxis>
</chrt:LineSeries.IndependentAxis>
<chrt:LineSeries.DependentRangeAxis>
<chrt:LinearAxis Orientation="Y" ShowGridLines="True" Minimum="-50" Maximum="50"></chrt:LinearAxis>
</chrt:LineSeries.DependentRangeAxis>
<chrt:LineSeries.Title>
<TextBlock TextAlignment="Center">Time<LineBreak/>vs.<LineBreak/>Random<LineBreak/>Data</TextBlock>
</chrt:LineSeries.Title>
</chrt:LineSeries>
</chrt:Chart>
<Button Name="btnStartStop" Width="Auto" Height="30" Grid.Row="1" HorizontalAlignment="Right" Margin="10" Click="btnStartStop_Click">Start Simulation</Button>
</Grid>