Getting started: Displaying multiple Cameras

I need to display n Cameras using CVB, how can I do that?

As this questions pops up every now and then, and most of the approaches to solve this task are lacking any design pattern “for the sake of keeping the example simple” I will here describe how to tackle this problem.

What I will show you here will be split into 2 parts.
After part 1 you have a simple WPF application that is able to display a fix (known at design time) number of camera streams.

Part 2 will then make the solution a little bit more dynamic, as we are going to add displays to the view depending on the number of cameras we have loaded (not fixed at design time).

We will do this in WPF, reasons pro and con WPF will not be part of this tutorial, just take it or leave it :wink:
Another decision already made is, that this example will be designed after the MVVM pattern.
This is a bit of overhead looking at the simplicity of what we want to achieve but experience shows, that most of you guys here will use the examples and tutorials as starting point and then add further functionality.
Having a clean separation between Logic (Models), displaying this data (View) and finally having a layer to connect the view with data provided by the model classes which is done in our ViewModels therefore is essential.

So lets dive right into it and start a new WPF application in Visual Studio:

  1. Create a new project:
Just a screenshot of which button to hit in case you have never done this

image

  1. Select WPF App (.Net Framework)
Framework to select

And finally hit the create button on the bottom right.

First thing to do now is to navigate through the Menustrip of VS and make one important change:
PROJECT → “YourProjectName” Properties → Build
Here, uncheck the Prefer32bit checkbox for all configurations.

Save!

Ready? Ok lets Start with the View then.
First of all, we are going to rename our MainWindow into MainView (to stick to MVVM naming conventions only). As this sometimes can be a bit tricky we will do this by deleting the MainWindow from the solution explorer (right click on MainWindow → delete) and then adding a new View:

How to add new View screenshot

Just name this “MainView”

Designing the UI

We want to have a way to load a new Camea and display, lets say 2 Cameras on our MainView.
So we will divide our MainView into a small strip on top where a MenuStrip will go and then vertically divide the rest of the View into 2 parts where our Display will go.
You do this by creating Grid definitions in your MainView.xaml:

Necessary Grid definitions

image

And the resulting view

Now its time to add our displays.

In order to have access to the :cvb: controls, we have to add :cvb: to our project references:
So on the right in the solution explorer right click on References and add Stemmer.Cvb and Stemmer.Cvb.Wpf from this path: “Your:cvb:Installation folder\ Lib\ Net”:

References to add:

And we have to add Cvb to the list of namespaces in our xaml code:

Add namespace to xaml

image

Then simply add 2 display controls to the MainView and align them next to each other by defining which column and row of the grid they are in:

Add displays in Xaml

image

Now we have to use our first data binding, to tell the display, which property to get the data from, that they should display:

Data Binding

image

Implement the ViewModel
Now that we have our View almost set up, its time to get our ViewModel running, so that we are able to actually bind to our properties from the View.
So just add a new class to your project and call it MainViewModel.

Adding a new class

Now in this class we have to creat our Image1 and Image2 property that we need for the display.
Also we will have a property for every Device we load.
As they will be returning a Stemmer.Cvb.Image, add Stemmer.Cvb to the usings.
Make sure that the class is public and in the classes constructor, just load the CVMock.vin for now to Mock a camera and assing its Image to our image properties.
The code for now will look like this:

MainViewModel so far
using Stemmer.Cvb;

namespace WpfApp3
{
  class MainViewModel
  {
    public MainViewModel()
    {
      Device1 = DeviceFactory.Open("CVMock.vin", 0, 0);
      Device2 = DeviceFactory.Open("CVMock.vin", 0, 1);
      Image1 = Device1.DeviceImage;
      Image2 = Device2.DeviceImage;
    }

    public Image Image1
    {
      get
      {
        return _image1;
      }
      set
      {
        _image1 = value;
      }
    }
    private Image _image1;

    public Image Image2
    {
      get
      {
        return _image2;
      }
      set
      {
        _image2 = value;
      }
    }
    private Image _image2;

    public Device Device1
    {
      get
      {
        return _device1;
      }
      set
      {
        _device1 = value;
      }
    }
    private Device _device1;

    public Device Device2
    {
      get
      {
        return _device2;
      }
      set
      {
        _device2 = value;
      }
    }
    private Device _device2;
  }
}

The properties are a little unhandy as you could write them shorter BUT we will need this structure later on.
Now you can start the application the first time, if you get the message, that the MainWindow can not be found, you probably messed up and renamed the MainWindow class that you should have deleted right away. If this is the case for you just change the App.xaml accordingly:

Yes, I have messed up also :sweat_smile:

If everything works, you will have an empty window, that we are going to fill now.
There are two reasons why this Window is still empty, one of which is, that we have not started to acquire images yet.
So lets just do this in an endless loop for now:

Just add the following Method to your MainViewModel and call it from the ctor for now.

Additional usings needed

using Stemmer.Cvb.Async;
using Stemmer.Cvb.Driver;
using System.Threading.Tasks

Acquisition logic
public async Task StartPlayingAsync(Device toStartStreamingFrom)
    {
      toStartStreamingFrom.Stream.Start();
      try
      {
        while (true)
        {
          using (StreamImage image = await toStartStreamingFrom.Stream.WaitAsync())
          {

          }
        }
      }
      finally
      {
        if (toStartStreamingFrom.Stream.IsRunning)
          toStartStreamingFrom.Stream.Abort();
      }
    }

Now we can start the Application once again aaaaaaand… still see nothing.
Sorry for this one :upside_down_face:
But, we are very close to finally seeing our camera images.
Yet we are missing some way to tell the View, that actually something in the ViewModel has changed.
To do this we have to add another class (the last one really, I promise) called ViewModelBase, with the following content (and let our MainViewModel derive from this class):

usings needed

using System.ComponentModel;
using System.Runtime.CompilerServices;

ViewModelBase implementation
 public abstract class ViewModelBase : INotifyPropertyChanged
  {
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
  }

Now in every property we have the possibility to tell the UI, when there were changes made from the ViewModel by calling OnPropertyChanged().
This will trigger an Update on the UI and with the following ViewModel code, we should finally be able to see our cameras running.
Also we have to tell our MainView, where to look for the properties we just created, by adding the MainViewModel as datacontext to the window:

image

The final code so far for the MainView:

MainView.xaml
<Window x:Class="WpfApp3.MainView"
        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:WpfApp3"
        xmlns:cvb="http://www.commonvisionblox.com/wpf"
        mc:Ignorable="d"
        Title="MainView" Height="450" Width="800">
  <Window.DataContext>
    <local:MainViewModel/>
  </Window.DataContext>
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="30"/>
      <RowDefinition/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition />
      <ColumnDefinition />
    </Grid.ColumnDefinitions>

    <cvb:Display Grid.Row="1" Grid.Column="0" Image="{Binding Image1}"/>
    <cvb:Display Grid.Row="1" Grid.Column="1" Image="{Binding Image2}"/>

  </Grid>
</Window>

And the ViewModel:

MainViewModel.cs
using Stemmer.Cvb;
using Stemmer.Cvb.Async;
using Stemmer.Cvb.Driver;
using System.Threading.Tasks;

namespace WpfApp3
{
  class MainViewModel : ViewModelBase
  {
    public MainViewModel()
    {
      Device1 = DeviceFactory.Open("CVMock.vin", 0, 0);
      Device2 = DeviceFactory.Open("CVMock.vin", 0, 1);
      Image1 = Device1.DeviceImage;
      Image2 = Device2.DeviceImage;
      StartPlayingAsync(Device1);
      StartPlayingAsync(Device2);
    }

    public Image Image1
    {
      get
      {
        return _image1;
      }
      set
      {
        _image1 = value;
        OnPropertyChanged();
      }
    }
    private Image _image1;

    public Image Image2
    {
      get
      {
        return _image2;
      }
      set
      {
        _image2 = value;
        OnPropertyChanged();
      }
    }
    private Image _image2;

    public Device Device1
    {
      get
      {
        return _device1;
      }
      set
      {
        _device1 = value;
        OnPropertyChanged();
      }
    }
    private Device _device1;

    public Device Device2
    {
      get
      {
        return _device2;
      }
      set
      {
        _device2 = value;
        OnPropertyChanged();
      }
    }
    private Device _device2;

    public async Task StartPlayingAsync(Device toStartStreamingFrom)
    {
      toStartStreamingFrom.Stream.Start();
      try
      {
        while (true)
        {
          using (StreamImage image = await toStartStreamingFrom.Stream.WaitAsync())
          {

          }
        }
      }
      finally
      {
        if (toStartStreamingFrom.Stream.IsRunning)
          toStartStreamingFrom.Stream.Abort();
      }
    }
  }
}

Just debug the application and you should have your two displays doing their job and displaying whatever you configured in the CVMock.vin.

In my case, the final result looks like this:

What about the Model?
You might have noticed at this point, that we are not having any Model classes in our MVVM approach.
This is just because we are not doing any processing or data handling until this point. Any processing of the images we are displaying should be done in a corresponing Model class.

So guys, thats it for now.
In the next post here on this thread we will finally add our Menu strip to decide which driver to load and have a Checkbox to start and stop streaming.

Happy coding!
Chris

4 Likes

As a smaller second step, before the larger changes to dynamically add displays, we’re going to add a menu item to start and stop the stream.

Adding the menu

For our purpose we only need one menu item that can be checked and unchecked. So we’re gonna create a menu with the “Aquisition” submenu containing the checkable “Grab” Item.

Adding menu to Xaml
    <Menu Grid.ColumnSpan="2">
      <MenuItem Height="30" Header="Aquisition">
        <MenuItem
          Header="Grab"
          IsCheckable="True"/>
      </MenuItem>
    </Menu>

Adding grabbing property

Now we need a property that binds to our checkbox and starts and stops the image aquisition. We start with a basic bool property. (Plus a check if the value actually changed before calling OnPropertyChanged(), which should always be done.)

Basic bool property
    public bool Grabbing
    {
      get => _grabbing;
      set
      {
        if (value != _grabbing)
        {
          _grabbing = value;
          OnPropertyChanged();
        }
      }
    }
    private bool _grabbing;

Then we add a call to our StartPlayingAsync() method to start the stream, when the checkbox gets checked:

Stream start
    public bool Grabbing
    {
      get => _grabbing;
      set
      {
        if (value != _grabbing)
        {          
          _grabbing = value;
          if (value)
          {
            StartPlayingAsync(Device1);
            StartPlayingAsync(Device2);
          }
          OnPropertyChanged();
        }
      }
    }
    private bool _grabbing;

To stop the stream when the checkbox gets unchecked we only need to change the while condition in StartPlayingAsync() to stop the aquisition when the Grabbing property changes to false:

Stream stop
public async Task StartPlayingAsync(Device toStartStreamingFrom)
    {
      toStartStreamingFrom.Stream.Start();
      try
      {
        while (Grabbing)
        {
          using (StreamImage image = await toStartStreamingFrom.Stream.WaitAsync())
          {

          }
        }
      }
      finally
      {
        if (toStartStreamingFrom.Stream.IsRunning)
          toStartStreamingFrom.Stream.Abort();
      }
    }

The last step is binding the property to the checkbox, just like the images were bound to the display.

Binding
    <Menu Grid.ColumnSpan="2">
      <MenuItem Height="30" Header="Aquisition">
        <MenuItem
          Header="Grab"
          IsCheckable="True"
          IsChecked="{Binding Grabbing}" />
      </MenuItem>
    </Menu>

Summary

Just to sync codes, here is the final version:

MainView.xaml
<Window
  x:Class="WpfApp1.MainView"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:cvb="http://www.commonvisionblox.com/wpf"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:local="clr-namespace:WpfApp1"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  Title="MainView"
  Width="800"
  Height="450"
  mc:Ignorable="d">
  <Window.DataContext>
    <local:MainViewModel />
  </Window.DataContext>
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="30" />
      <RowDefinition />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition />
      <ColumnDefinition />
    </Grid.ColumnDefinitions>

    <Menu Grid.ColumnSpan="2">
      <MenuItem Header="Aquisition" Height="30">
        <MenuItem Header="Grab" IsCheckable="True" IsChecked="{Binding Grabbing}"/>
      </MenuItem>
    </Menu>

    <cvb:Display
      Grid.Row="1"
      Grid.Column="0"
      Image="{Binding Image1}" />
    <cvb:Display
      Grid.Row="1"
      Grid.Column="1"
      Image="{Binding Image2}" />
  </Grid>
</Window>
MainViewModel.cs
using Stemmer.Cvb;
using Stemmer.Cvb.Async;
using Stemmer.Cvb.Driver;
using System.Threading.Tasks;

namespace WpfApp1
{
  public class MainViewModel : ViewModelBase
  {
    public MainViewModel()
    {
      Device1 = DeviceFactory.Open("CVMock.vin", 0, 0);
      Device2 = DeviceFactory.Open("CVMock.vin", 0, 1);
      Image1 = Device1.DeviceImage;
      Image2 = Device2.DeviceImage;
      StartPlayingAsync(Device1);
      StartPlayingAsync(Device2);
    }

    public bool Grabbing
    {
      get => _grabbing;
      set
      {
        if (value != _grabbing)
        {
          _grabbing = value;
          if (value)
          {
            StartPlayingAsync(Device1);
            StartPlayingAsync(Device2);
          }
          OnPropertyChanged();
        }
      }
    }
    private bool _grabbing;


    public Image Image1
    {
      get
      {
        return _image1;
      }
      set
      {
        _image1 = value;
        OnPropertyChanged();
      }
    }
    private Image _image1;

    public Image Image2
    {
      get
      {
        return _image2;
      }
      set
      {
        _image2 = value;
        OnPropertyChanged();
      }
    }
    private Image _image2;

    public Device Device1
    {
      get
      {
        return _device1;
      }
      set
      {
        _device1 = value;
        OnPropertyChanged();
      }
    }
    private Device _device1;

    public Device Device2
    {
      get
      {
        return _device2;
      }
      set
      {
        _device2 = value;
        OnPropertyChanged();
      }
    }
    private Device _device2;

    public async Task StartPlayingAsync(Device toStartStreamingFrom)
    {
      toStartStreamingFrom.Stream.Start();
      try
      {
        while (Grabbing)
        {
          using (StreamImage image = await toStartStreamingFrom.Stream.WaitAsync())
          {

          }
        }
      }
      finally
      {
        if (toStartStreamingFrom.Stream.IsRunning)
          toStartStreamingFrom.Stream.Abort();
      }
    }
  }
}

See you soon,
Lukas

2 Likes

Thank you @Lukas for pushing this topic forward.

Now, lets use the code we have at this point and do some processing.
Until now we only had 2 Displays, that did nothing else than display the image that was currently acquired.
After this little session we will have one display that shows an image that we manipulated in some way. We did some processing with the image if you will.

So… lets remove everything that had to do with the second device and only keep the Image2 property.

All that is left now should be this:

MainViewModel.cs
using Stemmer.Cvb;
using Stemmer.Cvb.Async;
using Stemmer.Cvb.Driver;
using System.Threading.Tasks;

namespace WpfApp3
{
  class MainViewModel : ViewModelBase
  {
    public MainViewModel()
    {
      Device1 = DeviceFactory.Open("CVMock.vin", 0, 0);
      Image1 = Device1.DeviceImage;
    }

    public bool Grabbing
    {
      get => _grabbing;
      set
      {
        if (value != _grabbing)
        {
          _grabbing = value;
          if (value)
          {
            StartPlayingAsync(Device1);
          }
          OnPropertyChanged();
        }
      }
    }
    private bool _grabbing;

    public Image Image1
    {
      get
      {
        return _image1;
      }
      set
      {
        _image1 = value;
        OnPropertyChanged();
      }
    }
    private Image _image1;

    public Image Image2
    {
      get
      {
        return _image2;
      }
      set
      {
        _image2 = value;
        OnPropertyChanged();
      }
    }
    private Image _image2;

    public Device Device1
    {
      get
      {
        return _device1;
      }
      set
      {
        _device1 = value;
        OnPropertyChanged();
      }
    }
    private Device _device1;    

    public async Task StartPlayingAsync(Device toStartStreamingFrom)
    {
      toStartStreamingFrom.Stream.Start();
      try
      {
        while (Grabbing)

        {
          using (StreamImage image = await toStartStreamingFrom.Stream.WaitAsync())
          {

          }
        }
      }
      finally
      {
        if (toStartStreamingFrom.Stream.IsRunning)
          toStartStreamingFrom.Stream.Abort();
      }
    }
  }
}

Now, the Image2 property still exists, our second display on the View also still exists and still binds to this property.
What we do now is, in the constructor we set the Image2 to a black image with the same size as Image1, just to have something else than blank white space on startup:

Image2 = new Image(new Size2D(Image1.Width, Image1.Height));
Image2.Initialize(0);

Bevore we can now use any filters on the image, we need to add a new reference and use it.
So go to your references, add Stemmer.Cvb.Foundation and dont forget to add it to the usings:

using Stemmer.Cvb.Foundation;

Short sync:

This is what the ctor should look like now
 public MainViewModel()
    {
      Device1 = DeviceFactory.Open("CVMock.vin", 0, 0);
      Image1 = Device1.DeviceImage;
      Image2 = new Image(new Size2D(Image1.Width, Image1.Height));
      Image2.Initialize(0);
    }

Now, lets go into the StartPlayingAsync(Device toStartStreamingFrom) and add some filtering on our camera images and display them on the second display:

StartPlayingAsync implementation with filter
public async Task StartPlayingAsync(Device toStartStreamingFrom)
    {
      toStartStreamingFrom.Stream.Start();
      try
      {
        while (Grabbing)
        {
          using (StreamImage image = await toStartStreamingFrom.Stream.WaitAsync())
          {
            Image2 = Filter.Sobel(image, FilterOrientation.Horizontal, FixedFilterSize.Kernel3x3);
          }
        }
      }
      finally
      {
        if (toStartStreamingFrom.Stream.IsRunning)
          toStartStreamingFrom.Stream.Abort();
      }
    }

Thats it… if you run your example right now and hit ‘Grab’ you should see something like this:

Now, that was simple, wasn’t it?

Well there is a little problem with this, that you might stumble upon in the future if you are doing some high speed processing or really complex image manipulation but you dont need to worry about this for now. If you are interested feel free to reveal the spoiler below.

There is just one thing we have to keep in mind whenever we do any processing.
Currently the Filtermethod takes 2ms-5ms (on my machine and in debug mode).
My CVMock.vin`s FrameRate is set to 5, this means, I get 5 images per second.
Thus I have 200ms from acquiring one image until the next image arrives.
So we are all good here.

If you aim at higher framerates or have more complex processing tasks, your processing might eventually take longer than the time until the next image is being acquired.
To handle this situations and prevent you from loosing images, :cvb: has a nice little RingBuffer interface, that by default is set to 3.
So, by default you are able to buffer 3 images for processing without data getting lost. Depending on how often your processing takes longer than the acquisition this might still not be enough.

For now, dont worry about this as it wont affect this tutorial, but if you like have a look at this explanation here:
https://forum.commonvisionblox.com/t/getting-started-with-cvb-net/246/14

Or just search the forum for RingBuffer.

Summary
As always, here is the MainViewModel to sync codes, we did not touch the MainView this time.

MainViewModel.cs
using Stemmer.Cvb;
using Stemmer.Cvb.Async;
using Stemmer.Cvb.Driver;
using System.Threading.Tasks;
using Stemmer.Cvb.Foundation;

namespace WpfApp3
{
  class MainViewModel : ViewModelBase
  {
    public MainViewModel()
    {
      Device1 = DeviceFactory.Open("CVMock.vin", 0, 0);
      Image1 = Device1.DeviceImage;
      Image2 = new Image(new Size2D(Image1.Width, Image1.Height));
      Image2.Initialize(0);
    }

    public bool Grabbing
    {
      get => _grabbing;
      set
      {
        if (value != _grabbing)
        {
          _grabbing = value;
          if (value)
          {
            StartPlayingAsync(Device1);
          }
          OnPropertyChanged();
        }
      }
    }
    private bool _grabbing;

    public Image Image1
    {
      get
      {
        return _image1;
      }
      set
      {
        _image1 = value;
        OnPropertyChanged();
      }
    }
    private Image _image1;

    public Image Image2
    {
      get
      {
        return _image2;
      }
      set
      {
        _image2 = value;
        OnPropertyChanged();
      }
    }
    private Image _image2;

    public Device Device1
    {
      get
      {
        return _device1;
      }
      set
      {
        _device1 = value;
        OnPropertyChanged();
      }
    }
    private Device _device1;    

    public async Task StartPlayingAsync(Device toStartStreamingFrom)
    {
      toStartStreamingFrom.Stream.Start();
      try
      {
        while (Grabbing)
        {
          using (StreamImage image = await toStartStreamingFrom.Stream.WaitAsync())
          {
            Image2 = Filter.Sobel(image, FilterOrientation.Horizontal, FixedFilterSize.Kernel3x3);
          }
        }
      }
      finally
      {
        if (toStartStreamingFrom.Stream.IsRunning)
          toStartStreamingFrom.Stream.Abort();
      }
    }
  }
}

Happy coding and see you soon!
Cheers!
Chris

Part 2: Dynamically adding displays

In this part we’re going to change our previous application, so we can open any number of devices and display their streams.

To start we need to restructure the ViewModels a bit. First we create a new DisplayViewModel, just like the MainViewModel was created. (Add a new class called DisplayViewModel, that derives from the ViewModelBase.)
The DisplayViewModel will be responsible for handling a single device and the
MainViewModel will then manage a collection of DisplayViewModels.

DisplayViewModel

We need all the current properties and the method from the MainViewModel, but with a few changes. We’ll start with the basically unchanged Device and Image properties:

Device and Image
    public Image Image
    {
      get
      {
        return _image;
      }
      set
      {
        _image = value;
        OnPropertyChanged();
      }
    }
    private Image _image;

    public Device Device
    {
      get
      {
        return _device;
      }
      set
      {
        _device = value;
        OnPropertyChanged();
      }
    }
    private Device _device;

Again we’ll need to add Stemmer.Cvb to the usings, for the Stemmer.Cvb.Image.

The StartPlayingAsync() method can be simplified a bit. As each DisplayViewModel only has one Device, we don’t need to give the method a device it can just use the property. We still need the same usings as last time:

Usings
using Stemmer.Cvb.Async;
using Stemmer.Cvb.Driver;
using System.Threading.Tasks;
StartPlayingAsync()
public async Task StartPlayingAsync()
    {
      Device.Stream.Start();
      try
      {
        while (Grabbing)
        {
          using (StreamImage image = await Device.Stream.WaitAsync())
          {

          }
        }
      }
      finally
      {
        if (Device.Stream.IsRunning)
          Device.Stream.Abort();
      }
    }

That also simplifies the Grabbing property:

Grabbing
public bool Grabbing
    {
      get => _grabbing;
      set
      {
        if (value != _grabbing)
        {
          _grabbing = value;
          if (value)
          {
            StartPlayingAsync();
          }
          OnPropertyChanged();
        }
      }
    }
    private bool _grabbing;

Now the only thing still missing is the constructor, where we set our Device and Image. Then the whole class should look like this:

Constructor
 public DisplayViewModel(Device device)
    {
      Device = device;
      Image = device.DeviceImage;
    }
Full DisplayViewModel
using Stemmer.Cvb;
using Stemmer.Cvb.Async;
using Stemmer.Cvb.Driver;
using System.Threading.Tasks;

namespace WpfApp3
{
  public class DisplayViewModel : ViewModelBase
  {
    public bool Grabbing
    {
      get => _grabbing;
      set
      {
        if (value != _grabbing)
        {
          _grabbing = value;
          if (value)
          {
            StartPlayingAsync();
          }
          OnPropertyChanged();
        }
      }
    }
    private bool _grabbing;


    public Image Image
    {
      get
      {
        return _image;
      }
      set
      {
        _image = value;
        OnPropertyChanged();
      }
    }
    private Image _image;

    public Device Device
    {
      get
      {
        return _device;
      }
      set
      {
        _device = value;
        OnPropertyChanged();
      }
    }
    private Device _device;

    public DisplayViewModel(Device device)
    {
      Device = device;
      Image = device.DeviceImage;
    }

    public async Task StartPlayingAsync()
    {
      Device.Stream.Start();
      try
      {
        while (Grabbing)
        {
          using (StreamImage image = await Device.Stream.WaitAsync())
          {

          }
        }
      }
      finally
      {
        if (Device.Stream.IsRunning)
          Device.Stream.Abort();
      }
    }
  }
}

DisplayView

Next we need a corresponding View for our new DisplayViewModel. Since the ViewModel contains only a single device, the View is very simple: A single display.

For the new DisplayView we’ll create a User Control this time because it won’t be its own window:

Right click the project → Add → User Control (WPF)

Then we need to again add the Cvb namespace, add a display and bind it to Image. The finished View should look like this:

Full DisplayView
<UserControl
  x:Class="WpfApp3.DisplayView"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:cvb="http://www.commonvisionblox.com/wpf"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:local="clr-namespace:WpfApp3"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  d:DesignHeight="450"
  d:DesignWidth="800"
  mc:Ignorable="d">
  <Grid>
    <cvb:Display Image="{Binding Image}" />
  </Grid>
</UserControl>

ObservableCollection

After those boring preparations we finally get to something new. The ObservableCollection is a collection that provides notifications when changes are made to it. Since we’ve been doing that job manually for our properties, you can probably see why it’s great for data bindings.

But first we need to get rid of all the old stuff in our MainViewModel since that’s all being done in the DisplayViewModel now. (Except the Grabbing property we can use that later)

Then we’ll add and initialize the ObservableCollection containing our DisplayViewModels. To use it we’ll need to add another using:

using System.Collections.ObjectModel;
public ObservableCollection<DisplayViewModel> Displays { get; } = new ObservableCollection<DisplayViewModel>();

Now we switch to the MainView to bind this Collection to a fitting control. But before that we’ll again need to remove some old stuff.
We no longer need the two displays and the ColumnDefinitions. Those only split the window for our two displays, and our new control will automatically fit as many displays as we need.

Then we can add our ItemsControl:

ItemsControl
<ItemsControl Grid.Row="1" ItemsSource="{Binding Displays}">
      <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
          <UniformGrid />
        </ItemsPanelTemplate>
      </ItemsControl.ItemsPanel>
      <ItemsControl.ItemTemplate>
        <DataTemplate>
          <local:DisplayView />
        </DataTemplate>
      </ItemsControl.ItemTemplate>
    </ItemsControl>

This Control allows us to bind a collection of items (our ObservableCollection Displays) and show them with the given DataTemplate (our DisplayView).

OpenDevice Command

Now that the basic structure is complete we can finally add the functionality to open a device in the MainViewModel. Since we only had property bindings until now. We’ll also learn how to bind methods with commands.

First we need the actual method to open a device and add a new DisplayViewModel with that device to our ObservableCollection:

OpenDevice method
    public void OpenDevice()
    {
      try
      {
        var dev = FileDialogs.LoadByDialog(path => DeviceFactory.Open(path), "CVB Devices|*.vin;*.emu;*.avi");
        if (dev == null)
          return;

        Displays.Add(new DisplayViewModel(dev));
      }
      catch (IOException ex)
      {
        MessageBox.Show(ex.Message, "Error loading device", MessageBoxButton.OK, MessageBoxImage.Error);
      }
    }

To bind that method to a menu item we’ll need to create a command. To make it easier, we first add a new class called DelegateCommand:

DelegateCommand
using System;
using System.Windows.Input;

namespace WpfApp3
{
  /// <summary>
  /// Object for executing an <see cref="Action{T}"/> with optional
  /// check if the <see cref="Action{T}"/> can be executed.
  /// </summary>
  public class DelegateCommand : ICommand
  {
    private readonly Predicate<object> _canExecute;
    private readonly Action<object> _execute;



    /// <summary>
    /// Eventhandler 
    /// </summary>
    public event EventHandler CanExecuteChanged;



    /// <summary>
    /// Constructor.
    /// </summary>
    /// <param name="execute"></param>
    public DelegateCommand(Action<object> execute)
                   : this(execute, null)
    {
    }



    /// <summary>
    /// Constructor with condition for executing action.
    /// </summary>
    /// <param name="execute">Action to execute.</param>
    /// <param name="canExecute">Check before executing.</param>
    public DelegateCommand(Action<object> execute,
                   Predicate<object> canExecute)
    {
      _execute = execute;
      _canExecute = canExecute;
    }



    /// <summary>
    /// Condition for executing.
    /// </summary>
    /// <param name="parameter"></param>
    /// <returns>True if null or condition is complied.</returns>
    public bool CanExecute(object parameter)
    {
      return _canExecute == null ? true : _canExecute(parameter);
    }



    /// <summary>
    /// Conducts the Action when command is called.
    /// </summary>
    /// <param name="parameter"></param>
    public void Execute(object parameter)
    {
      _execute(parameter);
    }



    /// <summary>
    /// Invokes action when event is raised.
    /// </summary>
    public void RaiseCanExecuteChanged()
    {
      CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }
  }
}

Then we add this using to the MainViewModel:

using System.Windows.Input;

After that we just create a ICommand property and initialize it in the constructor with the help of our DelegateCommand.

Creating the Command
public ICommand OpenDeviceCommand { get; }

public MainViewModel()
{
  OpenDeviceCommand = new DelegateCommand((o) => OpenDevice());
}

As a last step we create a new MenuItem in the MainView and bind the OpenDeviceCommand to it:

New MenuItem
    <Menu Grid.ColumnSpan="2">
      <MenuItem Height="30" Header="Aquisition">
        <MenuItem
          Header="Grab"
          IsCheckable="True"
          IsChecked="{Binding Grabbing}" />
      </MenuItem>
      <MenuItem Header="File">
        <MenuItem Command="{Binding OpenDeviceCommand}" Header="Open" />
      </MenuItem>
    </Menu>

We can finally see some progress:Try opening the CVMock.vin!

Starting/Stopping the stream on all displays

Now we just have to reenable the Grab button to work on all the displays. Since we didn’t delete the Grabbing property or its binding, we just have to change it a bit:

New Grabbing property
    public bool Grabbing
    {
      get => _grabbing;
      set
      {
        if (value != _grabbing)
        {
          _grabbing = value;

          foreach (var display in Displays)
            display.Grabbing = value;

          OnPropertyChanged();
        }
      }
    }
    private bool _grabbing;

Now it relays the changes made to it to the Grabbing properties of all the DisplayViewModels in our collection.

That’s it! Now you can test the Application.

To have the finished code in one place:

MainViewModel
using Stemmer.Cvb;
using Stemmer.Cvb.Wpf;
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Input;

namespace WpfApp3
{
  class MainViewModel : ViewModelBase
  {
    public ObservableCollection<DisplayViewModel> Displays { get; } = new ObservableCollection<DisplayViewModel>();

    public bool Grabbing
    {
      get => _grabbing;
      set
      {
        if (value != _grabbing)
        {
          _grabbing = value;

          foreach (var display in Displays)
            display.Grabbing = value;

          OnPropertyChanged();
        }
      }
    }
    private bool _grabbing;

    public ICommand OpenDeviceCommand { get; }

    public MainViewModel()
    {
      OpenDeviceCommand = new DelegateCommand((o) => OpenDevice());
    }

    public void OpenDevice()
    {
      try
      {
        var dev = FileDialogs.LoadByDialog(path => DeviceFactory.Open(path), "CVB Devices|*.vin;*.emu;*.avi");
        if (dev == null)
          return;

        Displays.Add(new DisplayViewModel(dev));
      }
      catch (IOException ex)
      {
        MessageBox.Show(ex.Message, "Error loading device", MessageBoxButton.OK, MessageBoxImage.Error);
      }
    }

  }
}
MainView
<Window
  x:Class="WpfApp3.MainView"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:cvb="http://www.commonvisionblox.com/wpf"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:local="clr-namespace:WpfApp3"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  Title="MainView"
  Width="800"
  Height="450"
  mc:Ignorable="d">
  <Window.DataContext>
    <local:MainViewModel />
  </Window.DataContext>
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="30" />
      <RowDefinition />
    </Grid.RowDefinitions>

    <Menu Grid.ColumnSpan="2">
      <MenuItem Height="30" Header="Aquisition">
        <MenuItem
          Header="Grab"
          IsCheckable="True"
          IsChecked="{Binding Grabbing}" />
      </MenuItem>
      <MenuItem Header="File">
        <MenuItem Command="{Binding OpenDeviceCommand}" Header="Open" />
      </MenuItem>
    </Menu>

    <ItemsControl Grid.Row="1" ItemsSource="{Binding Displays}">
      <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
          <UniformGrid />
        </ItemsPanelTemplate>
      </ItemsControl.ItemsPanel>
      <ItemsControl.ItemTemplate>
        <DataTemplate>
          <local:DisplayView />
        </DataTemplate>
      </ItemsControl.ItemTemplate>
    </ItemsControl>
  </Grid>
</Window>
DisplayViewModel
using Stemmer.Cvb;
using Stemmer.Cvb.Async;
using Stemmer.Cvb.Driver;
using System.Threading.Tasks;

namespace WpfApp3
{
  public class DisplayViewModel : ViewModelBase
  {
    public bool Grabbing
    {
      get => _grabbing;
      set
      {
        if (value != _grabbing)
        {
          _grabbing = value;
          if (value)
          {
            StartPlayingAsync();
          }
          OnPropertyChanged();
        }
      }
    }
    private bool _grabbing;


    public Image Image
    {
      get
      {
        return _image;
      }
      set
      {
        _image = value;
        OnPropertyChanged();
      }
    }
    private Image _image;

    public Device Device
    {
      get
      {
        return _device;
      }
      set
      {
        _device = value;
        OnPropertyChanged();
      }
    }
    private Device _device;

    public DisplayViewModel(Device device)
    {
      Device = device;
      Image = device.DeviceImage;
    }

    public async Task StartPlayingAsync()
    {
      Device.Stream.Start();
      try
      {
        while (Grabbing)
        {
          using (StreamImage image = await Device.Stream.WaitAsync())
          {

          }
        }
      }
      finally
      {
        if (Device.Stream.IsRunning)
          Device.Stream.Abort();
      }
    }
  }
}
DisplayView
<UserControl
  x:Class="WpfApp3.DisplayView"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:cvb="http://www.commonvisionblox.com/wpf"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:local="clr-namespace:WpfApp3"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  d:DesignHeight="450"
  d:DesignWidth="800"
  mc:Ignorable="d">
  <Grid>
    <cvb:Display Image="{Binding Image}" />
  </Grid>
</UserControl>
DelegateCommand
using System;
using System.Windows.Input;

namespace WpfApp3
{
  /// <summary>
  /// Object for executing an <see cref="Action{T}"/> with optional
  /// check if the <see cref="Action{T}"/> can be executed.
  /// </summary>
  public class DelegateCommand : ICommand
  {
    private readonly Predicate<object> _canExecute;
    private readonly Action<object> _execute;



    /// <summary>
    /// Eventhandler 
    /// </summary>
    public event EventHandler CanExecuteChanged;



    /// <summary>
    /// Constructor.
    /// </summary>
    /// <param name="execute"></param>
    public DelegateCommand(Action<object> execute)
                   : this(execute, null)
    {
    }



    /// <summary>
    /// Constructor with condition for executing action.
    /// </summary>
    /// <param name="execute">Action to execute.</param>
    /// <param name="canExecute">Check before executing.</param>
    public DelegateCommand(Action<object> execute,
                   Predicate<object> canExecute)
    {
      _execute = execute;
      _canExecute = canExecute;
    }



    /// <summary>
    /// Condition for executing.
    /// </summary>
    /// <param name="parameter"></param>
    /// <returns>True if null or condition is complied.</returns>
    public bool CanExecute(object parameter)
    {
      return _canExecute == null ? true : _canExecute(parameter);
    }



    /// <summary>
    /// Conducts the Action when command is called.
    /// </summary>
    /// <param name="parameter"></param>
    public void Execute(object parameter)
    {
      _execute(parameter);
    }



    /// <summary>
    /// Invokes action when event is raised.
    /// </summary>
    public void RaiseCanExecuteChanged()
    {
      CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }
  }
}
ViewModelBase
using System.ComponentModel;
using System.Runtime.CompilerServices;


namespace WpfApp3
{
  public abstract class ViewModelBase : INotifyPropertyChanged
  {
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
  }
}

Hopefully it was possible to follow this tutorial. Feel free to ask if you have any questions or problems.

Lukas

As @Lukas cant post images yet, this is what your application should look like with 4 dynamically created Displays: