Hello.
Thanks for previous answer but I can’t check how it work because of new problem. I develop WPF application and try to use MVVM as architecture pattern. In the Model part of my application I used a loop started in the new thread. In that loop I get some data which received to ViewModel and used as chart data. Looks as usual.
But then I tried to set visible range updated object which bound to axis I get exception ‘System.InvalidOperationException: ‘The calling thread cannot access this object because a different thread owns it.’ in my loop. I know it happens in WPF applications and need to use Dispatcher to solve it. But as I know if you try to use MVVM you not needed to use Dispatcher often because WPF-binding in most cases is thread-safe. That happend in my case? How to solve it?
Parts of my code:
XAML
<Window x:Class="HMI.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:s="http://schemas.abtsoftware.co.uk/scichart"
xmlns:localVM="clr-namespace:MyCom.HMI.ViewModel"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<localVM:HMIViewModel/>
</Window.DataContext>
<Window.Resources>
<localVM:InverseBooleanConverter x:Key="InverseBooleanConverter"/>
<localVM:ProcessStateToColorConverter x:Key="ProcessStateToColorConverter"/>
<localVM:ProcessStateToStartedConverter x:Key="ProcessStateToStartedConverter"/>
<localVM:ProcessStateToStoppedConverter x:Key="ProcessStateToStoppedConverter"/>
</Window.Resources>
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<StackPanel DockPanel.Dock="Left" Orientation="Vertical" Width="520">
<Border DockPanel.Dock="Top" Margin="5" Padding="5" BorderBrush="Black" BorderThickness="1,1,1,1" CornerRadius="5">
<StackPanel DockPanel.Dock="left" Orientation="Horizontal" Height="40">
<Ellipse Margin="5" Height="20" Width="20" HorizontalAlignment="Left" VerticalAlignment="Center" Stroke="Black" Fill="{Binding ProcessState, Converter={StaticResource ProcessStateToColorConverter}}"/>
<Label Content="State" Margin="5" Width="90" HorizontalContentAlignment="Left" VerticalContentAlignment="Center"/>
</StackPanel>
</Border>
<Border DockPanel.Dock="Top" Margin="5" Padding="5" BorderBrush="Black" BorderThickness="1,1,1,1" CornerRadius="5">
<Grid DockPanel.Dock="Left" Height="300">
<s:SciChartSurface ChartTitle="Profil" RenderableSeries="{s:SeriesBinding RenderableSeries}">
<s:SciChartSurface.XAxis>
<s:NumericAxis AxisTitle="Position, m" VisibleRange="{Binding XVisibleRange, Mode=TwoWay}"/>
</s:SciChartSurface.XAxis>
<s:SciChartSurface.YAxis>
<s:NumericAxis AxisTitle="Size, um" VisibleRange="{Binding YVisibleRange, Mode=TwoWay}"/>
</s:SciChartSurface.YAxis>
</s:SciChartSurface>
</Grid>
</Border>
</StackPanel>
...
ViewModel
namespace MyCom.HMI.ViewModel
{
public class HMIViewModel : BindableBase
{
private readonly HMIModel _model = new();
public HMIViewModel()
{
_renderableSeries = new ObservableCollection<IRenderableSeriesViewModel>();
((INotifyCollectionChanged)_model.TestControlData).CollectionChanged += (s, e) => { TestControlDataChanged(e); };
InitCharts();
}
...
private ObservableCollection<IRenderableSeriesViewModel> _renderableSeries;
public ObservableCollection<IRenderableSeriesViewModel> RenderableSeries
{
get { return _renderableSeries; }
set
{
SetProperty(ref _renderableSeries, value, nameof(RenderableSeries));
}
}
private IRange _xVisibleRange;
public IRange XVisibleRange
{
get => _xVisibleRange;
set
{
if (_xVisibleRange != value)
{
SetProperty(ref _xVisibleRange, value, nameof(XVisibleRange));
}
}
}
private IRange _yVisibleRange;
public IRange YVisibleRange
{
get => _yVisibleRange;
set
{
if (_yVisibleRange != value)
{
SetProperty(ref _yVisibleRange, value, nameof(YVisibleRange));
}
}
}
private XyDataSeries<double, double> _lineDataDiameter1;
...
private void InitCharts()
{ // TODO
_lineDataDiameter1 = new XyDataSeries<double, double>()
{
SeriesName = "Diameter1"
};
RenderableSeries.Add(new LineRenderableSeriesViewModel()
{
StrokeThickness = 2,
Stroke = Colors.SteelBlue,
DataSeries = _lineDataDiameter1,
StyleKey = "LineSeriesStyle"
});
}
private void TestControlDataChanged(NotifyCollectionChangedEventArgs args)
{
if (args.Action == NotifyCollectionChangedAction.Add && args.NewItems?.Count > 0)
{
var testControlActualState = args.NewItems.Cast<TestControlActualState>();
List<double> xValues = new();
List<double> yValuesDiameter1 = new();
foreach (var item in testControlActualState)
{
if (item.Diameter1 > 0f)
{
xValues.Add(item.FiberLength);
yValuesDiameter1.Add(item.Diameter1);
}
}
_lineDataDiameter1.Append(xValues, yValuesDiameter1);
// TODO
if (xValues.Count > 0)
{
var cuurMaxValueX = xValues.Max();
XVisibleRange.Max = cuurMaxValueX;
XVisibleRange.Min = cuurMaxValueX - 7000f > 0 ? cuurMaxValueX - 7000f : 0;
}
// TODO
if (yValuesDiameter1.Count > 0)
{
var cuurMaxValueY = yValuesDiameter1.Max();
YVisibleRange.Max = cuurMaxValueY + 50;
YVisibleRange.Min = 0;
}
}
}
Model
namespace MyCom.HMI.Model
{
public class HMIModel : BindableBase, IDisposable
{
private readonly ObservableCollection<TestControlActualState> _testControlData;
internal ReadOnlyObservableCollection<TestControlActualState> TestControlData { get; }
public HMIModel()
{
_testControlData = new ObservableCollection<TestControlActualState>();
TestControlData = new ReadOnlyObservableCollection<TestControlActualState>(_testControlData);
}
...
private void StartPollerThread()
{
_pollerCancellationToken ??= new CancellationTokenSource();
if (!_pollerCancellationToken.IsCancellationRequested)
{
Task.Factory.StartNew(() => PollerDoWork(_pollerCancellationToken.Token), TaskCreationOptions.LongRunning);
}
}
private void PollerDoWork(CancellationToken cancellationToken)
{
try
{
Thread.CurrentThread.Priority = ThreadPriority.Lowest;
IsPollerStarted = true;
while (!cancellationToken.IsCancellationRequested && (_isKeepConnection || _countOfTrying <= MAX_COUNT_OF_TRYING))
{
try
{
_testControlData.Add(_emulator.GetTestControlActualState());
if (!_isKeepConnection && _countOfTrying > 0)
{
Thread.Sleep(_pollerTimeout / REASK_COEFICIENT);
continue;
}
}
catch
{
// Thread must be alive!!! ...ALWAYS!!!
}
Thread.Sleep(_pollerTimeout);
}
}
finally
{
IsPollerStarted = false;
_pollerCancellationToken = null;
}
}
- Fedor Iudin asked 9 months ago
- last edited 9 months ago
-
When ViewModel line 98 executed I have error on Model’s line 46
- You must login to post comments
Hi there
To debug this we would need a small stand alone solution to reproduce it.
One thing I can say, in line #98 of your ViewModel you have this:
XVisibleRange.Max = cuurMaxValueX;
XVisibleRange.Min = cuurMaxValueX - 7000f > 0 ? cuurMaxValueX - 7000f : 0;
While this works its not advisable, it is best practice to update the entire DoubleRange object, e.g.
var min = cuurMaxValueX;
var max = cuurMaxValueX - 7000f > 0 ? cuurMaxValueX - 7000f : 0;
XVisibleRange = new DoubleRange(min, max); // Assuming XVisibleRange raises PropertyChanged
This will send a single INotifyPropertyChanged.PropertyChanged
event to the XAxis.VisibleRange
property and in WPF / MVVM all PropertyChanged events are marshalled to the UI thread
Whereas, when updating/setting XVisibleRange.Max
there is an internal event or callback which monitors this however it is not marshalled to the UI thread.
Try that. If you still have problems, then please send a code sample (minimal solution) to reproduce
Best regards
Andrew
- Andrew Burnett-Thompson answered 9 months ago
- You must login to post comments
Please login first to submit.