1

I have created it using winforms by just adding permon classes and downloading some realtime dasboards but i find it quite difficult in WPF

1 Answers1

4

There's a lot of tools that allows to draw various graphs in WPF.

But since i didn't find any manual graph drawing implementation, I've created the example - how to draw a graph in WPF using MVVM programming pattern.

0. Helper classes

For proper and easy MVVM implementation I will use 2 following classes.

NotifyPropertyChanged.cs - to notify UI on changes.

public class NotifyPropertyChanged : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

RelayCommand.cs - for easy commands use (for Button)

public class RelayCommand : ICommand
{
    private readonly Action<object> _execute;
    private readonly Predicate<object> _canExecute;

    public event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }

    public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
        => (_execute, _canExecute) = (execute, canExecute);

    public bool CanExecute(object parameter)
        => _canExecute == null || _canExecute(parameter);

    public void Execute(object parameter)
        => _execute(parameter);
}

1. Data implementation

As graph consists of contant amount of points an they're just turning around, I've implemented the Round-Robin Collection that has only one method Push().

RoundRobinCollection.cs

public class RoundRobinCollection : NotifyPropertyChanged
{
    private readonly List<float> _values;
    public IReadOnlyList<float> Values => _values;

    public RoundRobinCollection(int amount)
    {
        _values = new List<float>();
        for (int i = 0; i < amount; i++)
            _values.Add(0F);
    }

    public void Push(float value)
    {
        _values.RemoveAt(0);
        _values.Add(value);
        OnPropertyChanged(nameof(Values));
    }
}

2. Values collection to Polygon points Converter

Used in View markup

PolygonConverter.cs

public class PolygonConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        PointCollection points = new PointCollection();
        if (values.Length == 3 && values[0] is IReadOnlyList<float> dataPoints && values[1] is double width && values[2] is double height)
        {
            points.Add(new Point(0, height));
            points.Add(new Point(width, height));
            double step = width / (dataPoints.Count - 1);
            double position = width;
            for (int i = dataPoints.Count - 1; i >= 0; i--)
            {
                points.Add(new Point(position, height - height * dataPoints[i] / 100));
                position -= step;
            }
        }
        return points;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => null;
}

3. View Model

The main class containing all business logic of the App

public class MainViewModel : NotifyPropertyChanged
{
    private bool _graphEnabled;
    private float _lastCpuValue;
    private ICommand _enableCommand;

    public RoundRobinCollection ProcessorTime { get; }

    public string ButtonText => GraphEnabled ? "Stop" : "Start";

    public bool GraphEnabled
    {
        get => _graphEnabled;
        set
        {
            if (value != _graphEnabled)
            {
                _graphEnabled = value;
                OnPropertyChanged();
                OnPropertyChanged(nameof(ButtonText));
                if (value)
                    ReadCpu();
            }
        }
    }

    public float LastCpuValue
    {
        get => _lastCpuValue;
        set
        {
            _lastCpuValue = value;
            OnPropertyChanged();
        }
    }

    public ICommand EnableCommand => _enableCommand ?? (_enableCommand = new RelayCommand(parameter =>
    {
        GraphEnabled = !GraphEnabled;
    }));

    private async void ReadCpu()
    {
        try
        {
            using (PerformanceCounter cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total"))
            {
                while (GraphEnabled)
                {
                    LastCpuValue = cpuCounter.NextValue();
                    ProcessorTime.Push(LastCpuValue);
                    await Task.Delay(1000);
                }
            }
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.Message);
        }
    }

    public MainViewModel()
    {
        ProcessorTime = new RoundRobinCollection(100);
    }
}

Disclamer: async void is not recommended approach to make something async but here the usage is safe because any possible Exception will be handled inside. For more information about why async void is bad, refer to the documentation - Asynchronous Programming.

4. View

The whole UI markup of the app

<Window x:Class="CpuUsageExample.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:CpuUsageExample"
        mc:Ignorable="d"
        Title="MainWindow" Height="400" Width="800" >
    <Window.DataContext>
        <local:MainViewModel/><!-- attach View Model -->
    </Window.DataContext>
    <Window.Resources>
        <local:PolygonConverter x:Key="PolygonConverter"/><!-- attach Converter -->
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <WrapPanel>
            <Button Margin="5" Padding="10,0" Content="{Binding ButtonText}" Command="{Binding EnableCommand}"/>
            <TextBlock Margin="5" Text="CPU:"/>
            <TextBlock Margin="0, 5" Text="{Binding LastCpuValue, StringFormat=##0.##}" FontWeight="Bold"/>
            <TextBlock Margin="0, 5" Text="%" FontWeight="Bold"/>
        </WrapPanel>
        <Border Margin="5" Grid.Row="1" BorderThickness="1" BorderBrush="Gray" SnapsToDevicePixels="True">
            <Canvas ClipToBounds="True">
                <Polygon Stroke="CadetBlue" Fill="AliceBlue">
                    <Polygon.Resources>
                        <Style TargetType="Polygon">
                            <Setter Property="Points">
                                <Setter.Value>
                                    <MultiBinding Converter="{StaticResource PolygonConverter}">
                                        <Binding Path="ProcessorTime.Values"/>
                                        <Binding Path="ActualWidth" RelativeSource="{RelativeSource AncestorType=Canvas}"/>
                                        <Binding Path="ActualHeight" RelativeSource="{RelativeSource AncestorType=Canvas}"/>
                                    </MultiBinding>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </Polygon.Resources>
                </Polygon>
            </Canvas>
        </Border>
    </Grid>
</Window>

enter image description here

P.S. There's no code-behind

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
}
aepot
  • 4,558
  • 2
  • 12
  • 24
  • 1
    This is a great help , Thank you so much for this :-) – user8758891 Sep 02 '20 at 12:08
  • I am trying to add x (CPU) and y(dates for atleast 3-4 intervals) co-rdinates in this graph however no luck !!! – user8758891 Sep 02 '20 at 13:37
  • @user8758891 You may draw the grid on underlying Canvas ([example](https://stackoverflow.com/a/63495171/12888024)) – aepot Sep 02 '20 at 13:38
  • @user8758891 Adding text isn't a trivial job because you probably need to modify the RRC to store the timestamp of the each value. Then add some `ItemsControl` under the `Canvas` and bind it to the same RRC through another converter, or create some additional `ObservableCollection` what will contain coordinates for each `TextBlock` and text inside. Then modify the both collections as once. – aepot Sep 02 '20 at 13:50
  • @user8758891 did I answered the question? Did you check the [link](https://stackoverflow.com/a/31537844/12888024) at the top of the answer? If the question was answered, please accept the answer. If not, I'll delete the answer as unhelpful because I have no more ideas. – aepot Sep 02 '20 at 16:31
  • 1
    Thanks I have accepted , It helps me thanks once again – user8758891 Sep 02 '20 at 20:48