I am working on re-writing one of my previous toy project as MVVM structure using WPF. But binding does not work.
My project converts a .xef file to a .mat file. To follow MVVM structure, first I created a xef2matcore class to do the business logic, and provided several events (e.g. FileLoaded, ConversionProgressUpdated). Then I created a ViewModel class where the properties that will bind to the UI are stored (e.g. Progress, IsButtionEnabled). The ViewModel implemented the INotifyPropertyChanged interface. And finally the properties are bound to the UI elements (e.g. progressbar.Value, buttion.IsEnabled).
Here comes the problem: If the binding Property is changed directly by a call to the method defined in ViewModel, then the binding works fine. But if the binding Property is changed through a event handler method (and finally calls the same method defined in ViewModel), then the binding will not work (UI not updated even the binding Property value has been changed).
An explanation with the code is as below:
public class ViewModel: ObservableObject
{
private double _progress;
public double Progress
{
get => _progress;
set => Set(ref _progress, value);
}
private string _fileName;
public string FileName
{
get => _fileName;
set => Set(ref _fileName, value);
}
private bool _isButtonEnabled;
public bool IsButtonEnabled
{
get => _isButtonEnabled;
set => Set(ref _isButtonEnabled, value);
}
private bool _isBusy;
public bool IsBusy
{
get => _isBusy;
set => Set(ref _isBusy, value);
}
Xef2MatCore XCore;
public ViewModel()
{
Progress = 0;
FileName = "";
IsButtonEnabled = true;
}
private void XCore_FileLoaded()
{
IsButtonEnabled = false;
}
private void XCore_ProgressUpdated(double progress)
{
Progress = progress;
}
private void XCore_ExportFinished()
{
IsButtonEnabled = true;
Progress = 50;
}
public IAsyncCommand SelectFileCommandAsync { get => new AsyncCommand(UpdateFileSelectionExecuteAsync, CanFileSelectionExecute); }
private bool CanFileSelectionExecute() => !IsBusy;
private async Task UpdateFileSelectionExecuteAsync()
{
var folder_path = Environment.CurrentDirectory;
if (!Directory.Exists(folder_path)) Directory.CreateDirectory(folder_path);
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.InitialDirectory = Environment.CurrentDirectory;
openFileDialog.Filter = "Kinect Studio Data File|*.xef";
openFileDialog.RestoreDirectory = true;
openFileDialog.FilterIndex = 1;
var result = openFileDialog.ShowDialog();
if (result.HasValue && result.Value)
{
FileName = openFileDialog.FileName;
IsBusy = true;
XCore = new Xef2MatCore();
XCore.FileLoaded += XCore_FileLoaded;
XCore.ProgressUpdated += XCore_ProgressUpdated;
XCore.ExportFinished += XCore_ExportFinished;
//await Do_work();
await XCore.LoadAsync(FileName);
IsBusy = false;
}
else
{
return;
}
}
private async Task Do_work()
{
XCore_FileLoaded();
await Task.Run(() =>
{
int i = 0;
while (i < 100)
{
XCore_ProgressUpdated(i++);
Thread.Sleep(100);
}
});
XCore_ExportFinished();
}
}
The binding works if I uncomment await Do_work();
but does not work if I call await XCore.LoadAsync(FileName);
(which fires XCore_FileLoaded
, XCore_ProgressUpdated
and XCore_ExportFinished
through a event handler method).
The XAML file:
<Window x:Class="Xef2MatUI.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:Xef2MatUI"
mc:Ignorable="d"
Title="Xef2Mat Converter" >
<Window.DataContext>
<local:ViewModel x:Name="_viewmodel"/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
</Grid.RowDefinitions>
<Label Grid.Column="0" Grid.Row="0"
x:Name="label1"
Content="Select Kinect Studio (.xef) file:"
HorizontalAlignment="Left" VerticalAlignment="Center"/>
<Button Grid.Column="0" Grid.Row="1"
x:Name="button"
Content="Select"
IsEnabled="{Binding IsButtonEnabled, Mode=OneWay}"
HorizontalAlignment="Left" VerticalAlignment="Center"
Command="{Binding SelectFileCommand}"
/>
<Button Grid.Column="1" Grid.Row="1"
Content="Select"
/>
<Label Grid.Column="0" Grid.Row="2"
x:Name="label2" Content="Progress:"
HorizontalAlignment="Left" VerticalAlignment="Center"/>
<ProgressBar Grid.Column="0" Grid.Row="3"
x:Name="progressBar"
Value="{Binding Progress, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Column="1" Grid.Row="3"
x:Name="label3" Content="{Binding Progress, Mode=OneWay, StringFormat={}{0}%}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</Window>
Any help is appreciated.
As Selvin commented, when I called await XCore.LoadAsync(FileName);
, the time-consuming task blocked OnEventHappened
functions. Then separating them and putting the time-consuming task to a new thread solved the problem.