Search code examples
c#wpfreal-timesystem.reactivedynamic-data-display

Realtime Plotting with WPF using External Data


I'm creating a real-time WPF graphical plotter that will plot data points as it is received. It utilizes the Dynamic Data Display library. (http://dynamicdatadisplay.codeplex.com/)

Currently, the Application is set up so that it:

  • Updates the graphical plotter every time there is a change to the ObservableCollection that I have instantiated inside the WPF Application.

  • Adds data points using a custom method called AddDataPoint(...) which modifies the ObservableCollection.

The application runs as expected within the environment (when I hit F5 to Debug my Solution), but that is only because I am passing in "fake" data points to test the application with. I utilize an internal DispatchTimer/Random.Next(..) to continually feed in generated data points to the internally instantiated ObservableCollection; however, I have no idea how to allow for an external class or external data source to feed in "real" data to be graphed.

I'm really new to WPF and C# in general, so while I did do a lot of Google-ing on the subject I couldn't find a concrete answer (i.e. Data Binding -- it seems to me that it is only for use within the Application as well). Im stuck when it comes to passing in real-time data from an external source to my WPF Application.

I tried adding the WPF's .exe file as a resource to the external class/solution that will provide the data and starting an instance of the WPF by using:

      "WPFAppNameSpace".MainWindow w = new "WPFAppNameSpace".MainWindow();  
      w.Show();
      w.AddDataPoint(...);

But, it didn't work. The Window does not even show up! Can I have an external class (not a WPF App) pass in data to my WPF Graphing Application? If so, how can I go about doing this and what should I research?

Here are some code snippets from my Project:
XAML

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d3="http://research.microsoft.com/DynamicDataDisplay/1.0"
        Title="MainWindow" Height="480" Width="660" Loaded="Window_Loaded" WindowState="Maximized">
    <Grid>     
        <d3:ChartPlotter Name="plotter" Margin="12,10,12,14">

            <d3:ChartPlotter.MainHorizontalAxis>
                <d3:HorizontalAxis Name="xAxis"></d3:HorizontalAxis>
            </d3:ChartPlotter.MainHorizontalAxis>

            <d3:ChartPlotter.MainVerticalAxis>
                <d3:VerticalAxis Name="yAxis"></d3:VerticalAxis>
            </d3:ChartPlotter.MainVerticalAxis>

            <d3:VerticalAxisTitle Content="Voltage"/>
            <d3:HorizontalAxisTitle Content="Test Identifier"/>
            <d3:Header TextBlock.FontSize="20" Content="Dynamically Updated Graph"/>

        </d3:ChartPlotter>

    </Grid>

</Window>

MainWindow.xaml.cs

namespace WpfApplication1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        //Dummy Variables for Testing
        private readonly Random rand = new Random();
        private DispatcherTimer dt = new DispatcherTimer();

        ...

        //Data Sources for Graph
        private List<EnumerableDataSource<DataPoint>> enumSources;
        private List<ObservableCollection<DataPoint>> observableSources;
        private int dataSourceIndex = 0;

        public MainWindow()
        {
            InitializeComponent();
        }

        //Automatically Called after MainWindow() Finishes. Initlialize Graph.
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            //Initlizes Data Sources for Graph
            enumSources = new List<EnumerableDataSource<DataPoint>>();
            observableSources = new List<ObservableCollection<DataPoint>>();

            //Adds a new Source to the Graph (New Line on Graph)
            addNewSource("Test Data " + dataSourceIndex);

            //TESTING PURPOSES
            dt.Interval = new TimeSpan(25000);
            dt.Tick += new EventHandler(timerAction);
            dt.IsEnabled = true;
            dt.Start();
        }

        //Adds data into the observableSource and alerts the enumSource to add point into Graph
        private void AsyncAppend(...) {...}

        //Adds a new DataPoint onto the Graph and Specifies if it starts a new Line.
        public void AddDataPoint(..., bool newLine) {...}

        //Tests Function for Adding New Points/New Lines to Graph; Called by Timer
        private void timerAction(object sender, EventArgs e)
        { 
            //current count of points in a particular line
            var count = observableSources[dataSourceIndex].Count;
            if (count < 100)
            {
                //Adds Data to Current Line
                AddDataPoint(...);
            }
            else
            {
                //Starts New Line and Adds Data
                AddDataPoint(..., true);
            }
        }

        //Adds a new Data Source onto the Graph (Starts a New Line)
        private void addNewSource(string legendKey){...}

        //DataPoint Object to Pass into the Graph
        private class DataPoint
        {
            //X-Coord of the Point
            public double xCoord { get; set; }

            //Y-Coord of the Point
            public double yCoord { get; set; }

            //DataPoint's Label Name
            public string labelName { get; set; }

            //Constructor for DataPoint
            public DataPoint(double x, double y, string label = "MISSNG LBL")
            {
                xCoord = x;
                yCoord = y;
                labelName = label;
            }
        }
}

Solution

  • As an example of Reactive Extensions, here's a class that acquires data by simulation at a random interval between 0 and 5 seconds

    public class DataAquisitionSimulator:IObservable<int>
    {
        private static readonly Random RealTimeMarketData = new Random();
        public IDisposable Subscribe(IObserver<int> observer)
        {
            for (int i = 0; i < 10; i++)
            {
                int data = RealTimeMarketData.Next();
                observer.OnNext(data);
                Thread.Sleep(RealTimeMarketData.Next(5000));
            }
            observer.OnCompleted();
            return Disposable.Create(() => Console.WriteLine("cleaning up goes here"));
        }
    }
    

    It simulates acquiring market data (just random integers in this case) and posting them to an observer. It sleeps for a while between observations to simulate market latency.

    Here's a skeletal class that's been set up as a consumer...

    public class DataConsumer : IObserver<int>
    {
        private readonly IDisposable _disposable;
        public DataConsumer(DataAquisitionSimulator das)
        {
            _disposable = das.Subscribe(this);
            _disposable.Dispose();
        }
        public void OnCompleted()
        {
            Console.WriteLine("all done");
        }
        public void OnError(Exception error)
        {
            throw error;
        }
        public void OnNext(int value)
        {
            Console.WriteLine("New data " + value + " at " + DateTime.Now.ToLongTimeString());
        }
    }
    

    It implements the three methods needed and note that the 'OnNext' gets called upon each occurrence of new data. Presumably this class would be implemented in your VM and immediately inserts the new data into the binding pipeline so that users could visualize it.

    To see these two classes interacting, you can add this to a console app...

    static void Main(string[] args)
    {
        DataAquisitionSimulator v = new DataAquisitionSimulator();
        DataConsumer c = new DataConsumer(v);
    }
    

    Designing your threads is key, but otherwise this is a sample of how external data that has latency and irregular observations can be captured in a structured way.