using dispatcher to update UI from class with async methods in wpf - c#

I've got a class with manage data, and this class has a ObservableCollection which is bind in UI menu. The problem is that the observable collection loads data, but my UI does not show it.
My class is like this
public class DAL : INotifyPropertyChanged
{
public DAL()
{
this.unity = new UnitOfWork(#"http://192.168.0.173/vocalcontactapi");
}
private UnitOfWork unity;
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private ObservableCollection<EstadosAgente> estadosPausa = new ObservableCollection<EstadosAgente>();
public ObservableCollection<EstadosAgente> EstadosPausa
{
get { return this.estadosPausa; }
}
public async Task<bool> GetAgentStatesAsync()
{
await awaitGetAgentStatesTask();
OnPropertyChanged("EstadosPausa");
return true;
}
private Task awaitGetAgentStatesTask()
{
//UnitOfWork unity = new UnitOfWork(Properties.Settings.Default.restServer);
NameValueCollection parms = new NameValueCollection();
parms.Add("servicioId", "1");
return Task.Run(() =>
{
try
{
var estados = unity.EstadosAgente.GetAll(parms).Where(q => q.habilitado == true).Select(p => p).ToList();
if (estados == null)
return;
estados.ForEach(x =>
{
Application.Current.Dispatcher.Invoke(new Action(() =>
{
this.EstadosPausa.Add(x); //*** I think here is the problem***
}));
});
}
catch (Exception ex)
{
string err = ex.Message;
}
});
}
}
And mainWindow I have a Property of class DAL:
private DAL data = new DAL();
public DAL Data { get{ return this.data}}
In my menu I've got next:
<Menu Grid.Row="1">
<MenuItem Header="uno" ItemsSource="{Binding DAL.EstadosPausa}" Click="DataBoundMenuItem_Click">
<MenuItem.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding estado}"/>
<Setter Property="Tag" Value="{Binding}" />
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
</Menu>
Obviously, all properties of the loaded data are correct. Any help please?

There's few things in this code which makes me "itchy" but anyway...
You have public DAL Data property but you are binding to DAL.EstadosPausa. You need to bind to Data.EstadosPausa and that'll solve it. You'll see all binding errors in your debuggers Output window.
Here, consider this simplified yet 100% working version of your code base. Now you are fooling around with async/awaits, returning Task<bool> and Dispatching work to UI thread for no good reason (that is obvious from your example).
XAML
<Menu Grid.Row="1">
<MenuItem Header="Uno" ItemsSource="{Binding Data.EstadosPausa}">
<MenuItem.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Estado}"/>
<Setter Property="Tag" Value="{Binding}" />
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
</Menu>
CodeBehind
public partial class MainWindow : Window
{
private readonly Dal _data = new Dal();
public MainWindow()
{
InitializeComponent();
DataContext = this;
_data.GetAgentStatesAsync(); // Fire Task away, no time to wait!!1
}
public Dal Data { get { return _data; } }
}
public class EstadosAgente
{
public string Estado { get; set; }
}
public class Dal : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public Task GetAgentStatesAsync()
{
return Task.Run(() =>
{
Thread.Sleep(1000); // I'm a long running operation...
var estados = new List<EstadosAgente>
{
new EstadosAgente { Estado = "Estado 1" },
new EstadosAgente { Estado = "Estado 2" }
};
EstadosPausa = new ObservableCollection<EstadosAgente>(estados);
OnPropertyChanged("EstadosPausa");
});
}
public ObservableCollection<EstadosAgente> EstadosPausa { get; private set; }
private void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged; // <- always assign to local variable!
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}

Related

Prevent WPF ViewModel from creating new instance when navigating to other views

I am attempting to prevent my application from deleting a view and then creating a new one each time it's navigated around. I have a dashboard that will run a test program, if I select the settings view, then back to the dashboard, it has deleted the running test and initialized a new view. I need to keep the same view instance alive so that the test can continue to run while the user navigates to the settings view and back again but I cant exactly figure out how to successfully do that. I have attempted making the instance static but that doesn't seem to make a difference.
MainViewModel
class MainVM : ViewModelBase
{
private object _currentView;
public object CurrentView
{
get { return _currentView; }
set { _currentView = value; OnPropertyChanged(); }
}
public ICommand DashboardCommand { get; set; }
public ICommand SettingsCommand { get; set; }
public static DashboardVM DashboardInstance { get; } = new DashboardVM();
public static SettingsVM SettingsInstance { get; } = new SettingsVM();
private void Dashboard(object obj) => CurrentView = DashboardInstance;
private void Settings(object obj) => CurrentView = SettingsInstance;
public MainVM()
{
DashboardCommand = new RelayCommand(Dashboard);
SettingsCommand = new RelayCommand(Settings);
// Startup Page
CurrentView = DashboardInstance;
}
}
ViewModelBase
public partial class ViewModelBase : ObservableObject, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string propName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
public void NotifyPropertyChanged(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
MainView - Navigation
<!-- Navigation Panel -->
<Grid HorizontalAlignment="Left" Width="76">
<Border Background="#3D5A8A" CornerRadius="10,0,0,10" />
<StackPanel Height="1200" Width="76">
<!-- Dashboard Button -->
<nav:Button Style="{StaticResource NavButton_Style}"
Command="{Binding DashboardCommand}"
IsChecked="True">
<Grid>
<Image Source="Images/dash_black_50.png"
Style="{StaticResource NavImage_Style}" />
<TextBlock Text="Dashboard"
Style="{StaticResource NavText_Style}" />
</Grid>
</nav:Button>
<!-- Settings Button -->
<nav:Button Style="{StaticResource NavButton_Style}"
Command="{Binding SettingsCommand}">
<Grid>
<Image Source="Images/gear_black_50.png"
Style="{StaticResource NavImage_Style}" />
<TextBlock Text="Settings"
Style="{StaticResource NavText_Style}" />
</Grid>
</nav:Button>
</StackPanel>
</Grid>
DashboardVM
class DashboardVM : ViewModelBase
{
enum TestItemStatus
{
Reset,
Queued,
InProgress,
Pass,
Fail
}
private readonly PageModel _pageModel;
private string _StartButtonText,
_WaveRelayEthernetText;
private bool isTestRunning;
public DashboardVM()
{
_pageModel = new PageModel();
_StartButtonText = "Start Test";
_WaveRelayEthernetText = string.Empty;
StartButtonCommand = new RelayCommand(o => StartButtonClick("StartButton"));
}
#region Text Handlers
public string StartButtonText
{
get { return _StartButtonText; }
set { _StartButtonText = value; NotifyPropertyChanged("StartButtonText"); }
}
public string WaveRelayEthernetText
{
get { return _WaveRelayEthernetText; }
set { _WaveRelayEthernetText = value; NotifyPropertyChanged("WaveRelayEthernetText"); }
}
#endregion
private bool TestRunning
{
get { return isTestRunning; }
set { isTestRunning = value;
if (isTestRunning) { StartButtonText = "Stop Test"; }
else { StartButtonText = "Start Test";
ResetTestItems();
}
NotifyPropertyChanged("TestRunning");
}
}
public ICommand StartButtonCommand { get; set; }
private void StartButtonClick(object sender)
{
if(TestRunning)
{
TestRunning = false;
}
else
{
SetTestItemsToQueued();
MessageBox.Show("Please plug in Tube 1");
// Start program.
TestRunning = true;
WaveRelayEthernetText = TestItemStatusEnumToString(TestItemStatus.InProgress);
}
}
private string TestItemStatusEnumToString(TestItemStatus temp)
{
if (temp == TestItemStatus.Reset) { return string.Empty; }
else if (temp == TestItemStatus.Queued) { return "Queued"; }
else if (temp == TestItemStatus.InProgress) { return "In Progress"; }
else if (temp == TestItemStatus.Pass) { return "Pass"; }
else if (temp == TestItemStatus.Fail) { return "Fail"; }
else { return string.Empty; }
}
private void SetTestItemsToQueued()
{
WaveRelayEthernetText = TestItemStatusEnumToString(TestItemStatus.Queued);
}
private void ResetTestItems()
{
WaveRelayEthernetText = TestItemStatusEnumToString(TestItemStatus.Reset);
}
}
Image for reference:
My Issue was in the App.xaml, I link a DataTemplate file like this:
<ResourceDictionary Source="Utilities/DataTemplate.xaml" />
Inside the data template, I had this code that linked the views to the view models.
<ResourceDictionary [...]">
<DataTemplate DataType="{x:Type vm:DashboardVM}">
<view:Dashboard />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:SettingsVM}">
<view:Settings />
</DataTemplate>
</ResourceDictionary>
I changed that code to link the two to this:
<ResourceDictionary [...]>
<view:Dashboard x:Key="DashboardViewKey"/>
<view:Settings x:Key="SettingsViewKey"/>
<DataTemplate DataType="{x:Type vm:DashboardVM}">
<ContentControl Content="{StaticResource DashboardViewKey}" />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:SettingsVM}">
<ContentControl Content="{StaticResource SettingsViewKey}" />
</DataTemplate>
</ResourceDictionary>
I am now receiveing the expected behavior of being able to navigate without the Dashboard constructor being called, thus the view does not destory and recreate.
I hope someone else finds this useful.

How to go from one tab to another tab in AvalonDock when a button or key is pressed?

I created a view from SampleDockWindowView and I like to go to the other view created window when a button is pressed.
I've already tried Application.Current.Windows. This array is just empty.
window2 win2= new window2();
win2.Show();
I would image something like this but with avalondock tabs. not necessarily new window but just to show the existing window when a button is pressed
The following example shows how to cycle through all document tabs of the custom DocumentManager when a Ctrl + Right key combination is pressed (in this example the DocumentManager control is required to have focus):
MainWindow.xaml
<Window>
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<Grid>
<!--
Border to capture keyboard events of the DocumentManager UserControl.
To capture keys in a different scope i.e. more globally,
move the input bindings to a parent control.
-->
<Border>
<!-- Bind keyboard keys to a ICommand. -->
<Border.InputBindings>
<KeyBinding Key="Right"
Modifiers="Control"
Command="{Binding NavigateToNextDocument}"/>
</Border.InputBindings>
<DocumentManager x:Name="DocumentManager" />
</Border>
</Grid
</Window>
DocumentManager.xaml
<UserControl>
<Grid>
<xceed:DockingManager DocumentsSource="{Binding DocumentMainPool}">
<xceed:DockingManager.LayoutItemTemplate>
<DataTemplate DataType="{x:Type local:Document}">
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</xceed:DockingManager.LayoutItemTemplate>
<xceed:DockingManager.LayoutItemContainerStyle>
<Style TargetType="xcad:LayoutItem">
<Setter Property="Title"
Value="{Binding Model.Title}" />
<Setter Property="ToolTip"
Value="{Binding Model.Title}" />
<!-- Important -->
<Setter Property="IsSelected"
Value="{Binding Model.IsSelected, Mode=TwoWay}" />
</Style>
</xceed:DockingManager.LayoutItemContainerStyle>
<xceed:LayoutRoot>
<xceed:LayoutPanel>
<xceed:LayoutDocumentPaneGroup>
<!-- *** Dynamically created content (by view model) *** -->
<xceed:LayoutDocumentPane />
</xceed:LayoutDocumentPaneGroup>
</xceed:LayoutPanel>
</xceed:LayoutRoot>
</xceed:DockingManager>
</Grid>
</UserControl>
MainViewModel.cs
class MainViewModel : INotifyProeprtyChanged
{
public MainViewModel()
{
this.DocumentMainPool = new ObservableCollection<IDocument>()
{
new Document("First Document"),
new Document("Second Document")
};
}
private ObservableCollection<IDocument> documentMainPool;
public ObservableCollection<IDocument> DocumentMainPool
{
get => this.documentMainPool;
set
{
this.documentMainPool = value;
OnPropertyChanged();
}
}
public ICommand NavigateToNextDocument => new RelayCommand(param => CycleNextDocuments());
private void CycleNextDocuments()
{
// Only one or no document -> nothing to cycle through
if (this.DocumentMainPool.Count < 2)
{
return;
}
IDocument currentlySelectedDocument = this.DocumentMainPool.FirstOrDefault(document => document.IsSelected);
int currentDocumentIndex = this.DocumentMainPool.IndexOf(currentlySelectedDocument);
// If last document reached, show first again
if (currentDocumentIndex == this.DocumentMainPool.Count - 1)
{
this.DocumentMainPool.FirstOrDefault().IsSelected = true;
return;
}
IDocument nextDocument = this.DocumentMainPool
.Skip(currentDocumentIndex + 1)
.Take(1)
.FirstOrDefault();
nextDocument.IsSelected = true;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
IDocument.cs
// Important: IsSelected must raise PropertyChanged
public interface IDocument : INotifyPropertyChanged
{
string Title { get; set; }
bool IsSelected { get; set; }
}
Document.cs
public class Document : IDocument
{
public Document(string title)
{
this.Title = title;
}
#region Implementation of IDocument
public string Title { get; set; }
private bool isSelected;
public bool IsSelected
{
get => this.isSelected;
set
{
this.isSelected = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
RelayCommand.cs
Implementation taken from Microsoft Docs: Patterns - WPF Apps With The Model-View-ViewModel Design Pattern:
public class RelayCommand : ICommand
{
#region Fields
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
#endregion // Fields
#region Constructors
public RelayCommand(Action<object> execute) : this(execute, null) { }
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute; _canExecute = canExecute;
}
#endregion // Constructors
#region ICommand Members
[DebuggerStepThrough]
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter) { _execute(parameter); }
#endregion // ICommand Members
}
Here is a solution that works with MVVM which makes sense since it is an MVVM framework :-) (have not tried it myself, yet, but the code looks sane).

MVVM, WPF changing view after double click

I have Grid with tableView with data from excel file. I want to assign a model from another grid (selected in another tableview) after double clicking.
My code works but the user sees the change only after some changes on the tableview (eg sort by column).
Somy view
<Grid PreviewKeyDown="Grid_PreviewKeyDown2">
<dxg:GridControl x:Name="grid"
AutoGenerateColumns="AddNew"
EnableSmartColumnsGeneration="True"
SelectionMode="Row"
ItemsSource="{Binding ListaZExcela, Mode=TwoWay}"
SelectedItem="{Binding ZaznaczonyDokument}">
<dxg:GridControl.View>
<dxg:TableView x:Name="view"
ShowFixedTotalSummary="True"
AllowGrouping="False"
AllowEditing="False"
ShowGroupPanel="False">
<dxg:TableView.InputBindings>
<MouseBinding Gesture="LeftDoubleClick"
Command="{Binding DoubleClickCommand}">
<!--<MouseBinding.CommandParameter>
<MultiBinding Converter="{StaticResource Convert}">
<Binding ElementName="grid" Path="SelectedItem"/>
<Binding ElementName="gridKontrah" Path="SelectedItem"/>
</MultiBinding>
</MouseBinding.CommandParameter>-->
</MouseBinding>
</dxg:TableView.InputBindings>
</dxg:TableView>
</dxg:GridControl.View>
</dxg:GridControl>
</Grid>
my viewmodel
public class MainViewModel : ViewModelBase, INotifyPropertyChanged
{
public ObservableCollection<DokumentModel> _listaZExcela;
public ObservableCollection<DokumentModel> _listaZExcelaPom;
public ObservableCollection<KontrahentModel> _listaKontrahent;
private BackgroundWorker _worker;
private BackgroundWorker _worker2;
private int _progressPercentage = 0;
private bool _startEnabled = true;
private bool _cancelEnabled = false;
public KontrahentModel _zaznaczonyKontrahent;
public DokumentModel _zaznaczonyDokument;
public string _filePath;
public MainViewModel()
{
Opencmd = new DelegateCommand(OpenCmd);
//DoubleClickCommand = new DelegateCommand(DoubleClickCmd);
importujCommand = new DelegateCommand(StartProcess2);
DoubleClickCommand = new DelegateCommand(ExecuteMy);
_listaZExcela = new ObservableCollection<DokumentModel>();
_listaZExcelaPom = new ObservableCollection<DokumentModel>();
_listaKontrahent = new ObservableCollection<KontrahentModel>();
DajKontrahentow();
_zaznaczonyKontrahent = new KontrahentModel();
_zaznaczonyDokument = new DokumentModel();
}
public ICommand DoubleClickCommand { get; set; }
public void ExecuteMy()
{
ZaznaczonyDokument.Kontrahent = ZaznaczonyKontrahent;
ZaznaczonyDokument.KontrahentNazwaskr = ZaznaczonyKontrahent.NazwaSkr;
}
#endregion
#region OnPropertyChangedRegion
public KontrahentModel ZaznaczonyKontrahent
{
get
{
return _zaznaczonyKontrahent;
}
set
{
_zaznaczonyKontrahent = value;
OnParameterChanged("ZaznaczonyKontrahent");
}
}
public DokumentModel ZaznaczonyDokument
{
get
{
return _zaznaczonyDokument;
}
set
{
_zaznaczonyDokument = value;
OnParameterChanged("ZaznaczonyDokument");
}
}
public ObservableCollection<DokumentModel> ListaZExcela
{
get
{
return _listaZExcela;
}
set
{
_listaZExcela = value;
OnPropertyChanged("ListaZExcela");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(params string[] nazwy)
{
if (PropertyChanged != null)
{
foreach (string nazwaWlasnosci in nazwy)
PropertyChanged(this, new
PropertyChangedEventArgs(nazwaWlasnosci));
}
}
#endregion
}
}
Data assignment is in ExecuteMy. I did not paste some of the code to make it more clean

How to update listbox with progress bar GUI during execution

I have a listbox in a wpf window thats bound to a list in a viewmodel object. When I run a method in the viewmodel object it processes members of the list and each member has a progress. I would like to have the gui update continuously during execution. As it is now, it only updates gui when the processing is finished.
Here I have tried to create a small example of what I have right now:
MainWindow.xaml:
<Window x:Class="WPF_MVVM_Thread_Progressbar.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WPF_MVVM_Thread_Progressbar"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:TestViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="8*"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="8*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListBox Grid.Column="0" Grid.Row="0" Margin="5" ItemsSource="{Binding TestWorker.TestList}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<ProgressBar Minimum="0" Maximum="100" Value="{Binding Progress, Mode=OneWay}" Background="Bisque">
<ProgressBar.Style>
<Style TargetType="{x:Type ProgressBar}">
<Style.Triggers>
<DataTrigger Binding="{Binding Progress}" Value="0">
<Setter Property="Visibility" Value="Hidden"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ProgressBar.Style>
</ProgressBar>
<TextBlock FontWeight="Bold" Text="{Binding Path=Name}" Background="Transparent"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Grid.Column="0" Grid.Row="1" Content="TestRun" Command="{Binding TestRunCommand}"></Button>
<TextBlock Text="{Binding SelectedIdx}" Grid.Column="1" Grid.Row="1"/>
</Grid>
</Window>
MainWindowl.xaml.cs:
using Prism.Commands;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
namespace WPF_MVVM_Thread_Progressbar
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
public class TestViewModel : INotifyPropertyChanged
{
private WorkingClass _testWorker;
private DelegateCommand _testRunCommand;
public DelegateCommand TestRunCommand
{
get { return _testRunCommand; }
set { _testRunCommand = value; }
}
public WorkingClass TestWorker
{
get { return _testWorker; }
set { _testWorker = value; RaisePropertyChanged("TestWork"); }
}
private int _selectedIdx;
public int SelectedIdx
{
get { return _selectedIdx; }
set { _selectedIdx = value; RaisePropertyChanged("SelectedIdx"); }
}
public TestViewModel()
{
_testWorker = new WorkingClass();
_testRunCommand = new DelegateCommand(TestRun, canRun);
}
public async void TestRun()
{
//await Task.Run(() => _testWorker.Work());
_testWorker.Work();
}
private bool canRun()
{
return true;
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class WorkingClass : INotifyPropertyChanged
{
private ObservableCollection<TestObject> _testList;
public ObservableCollection<TestObject> TestList
{
get { return _testList; }
set { _testList = value; RaisePropertyChanged("TestList"); }
}
public WorkingClass()
{
_testList = new ObservableCollection<TestObject>();
_testList.Add(new TestObject("Object A"));
_testList.Add(new TestObject("Object B"));
_testList.Add(new TestObject("Object C"));
RaisePropertyChanged("TestList");
}
public void Work()
{
foreach (var obj in TestList)
{
obj.TestWork();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class TestObject : INotifyPropertyChanged
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
private int _progress;
public int Progress
{
get { return _progress; }
set { _progress = value; RaisePropertyChanged("Progress"); }
}
public TestObject(string name)
{
this._name = name;
_progress = 0;
}
public void TestWork()
{
for (int i = 0; i < 100; i++)
{
System.Threading.Thread.Sleep(10);
Progress++;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
I have tried to use ObservableCollection and INotifyPropertyChanged but this it seems not to be enough.
Eventually I would like to be able to have the same effect using async/await call from the TestViewModel.TestRun().
Could someone perhaps offer some insights on this? It would be much appreciated.
I think the current reason that you have the UI only updating once completed, is that you are running all of this on the UI thread. I would instead try this:
Task.Run(async delegate
{
await _testWorker.Work();
});
Or
Task.Run(() =>
{
_testWorker.Work();
});
Or
Task.Factory.StartNew(() =>
{
_testWorker.Work();
});
Or
var newThread = new Thread(new ThreadStart(_testWorker.Work());
newThread.Start();
This will return back to the UI instantly but allow your code to continue.
Note: You will have to be careful about the use of objects off the UI thread. ObservableCollections can only be created on the same thread as the dispatcher that handles the UI work. If you are using two-way binding, again you have to be careful about thread safety.
I've successfully done this in the past using a BackgroundWorker.
public class TestObject : INotifyPropertyChanged {
private BackgroundWorker worker;
public TestObject() {
worker = new BackgroundWorker() {
WorkerReportsProgress = true
};
worker.DoWork += DoWork;
worker.ProgressChanged += WorkProgress;
worker.RunWorkerCompleted += WorkFinished;
}
public int Progress
{
get { return _progress; }
set { _progress = value; RaisePropertyChanged("Progress"); }
}
// Begin doing work
public void TestWork() {
worker.RunWorkerAsync();
}
private void DoWork(object sender, DoWorkEventArgs eventArgs) {
worker.ReportProgress(0, "Work started");
for (int i = 0; i < 100; i++) {
System.Threading.Thread.Sleep(10);
worker.ReportProgress(i, "Message");
}
}
// Fires when the progress of a job changes.
private void WorkProgress(object sender, ProgressChangedEventArgs e) {
// Do something with the progress here
Progress = e.ProgressPercentage;
}
// Fires when a job finishes.
private void WorkFinished(object sender, RunWorkerCompletedEventArgs e) {
// The work finished. Do something?
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
// NOTE: If you're running C#6 use the null conditional operator for this check.
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(e));
}
}
A BackgroundWorker basically runs everything on a separate thread and reports back when its progress changes or it finishes working. You can pull out the ProgressPercentage from its progress report and use that in the UI. Hope that helps. To keep the example simple I didn't include some of your code but that should given you an idea of how it can be done.

WPF CommandParameter binding fails in ToolBar?

Here's the situation: It's a bit hard to describe, so skip to the steps to recreate and copy/paste the code into a new project if you want. ListViewModel contains a list of ViewModel (Items) and a list of ICommand (Actions). MainWindow has a ToolBar that binds to the Actions, a ListView that binds to the Items, and a ContextMenu that binds to the actions within the ListView. In my implementation of ICommand (Command.cs), I have added the ability to insert custom code (the OnIsVisible property) that checks if the command Visibility should be Visible or Collapsed. This code works great for the Actions in the ToolBar and ContextMenu until you open the ContextMenu. Then the CommandParameter for the ToolBar is forever null, except when the ContextMenu is open.
Steps to recreate:
Select an item in the ListView
Click "Show When Selected" in the ContextMenu
Select another item in the ListView
At this point, the CommandParameter binding will always be NULL to the command object. So the "Show When Selected" button in the ToolBar will no longer appear.
Code:
In a new WPF application project called "NullParameter", create/edit the following files...
MainWindow.xaml:
<Window x:Class="NullParameter.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="IsSelected"
Value="{Binding IsSelected}" />
</Style>
<Style x:Key="contextMenuItemStyle"
TargetType="{x:Type MenuItem}">
<Setter Property="Header"
Value="{Binding Header}" />
<Setter Property="Command"
Value="{Binding}" />
<Setter Property="Visibility"
Value="{Binding Visibility}" />
<Setter Property="CommandParameter"
Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}, Path=PlacementTarget.Tag.SelectedItems}" />
</Style>
<DataTemplate x:Key="toolBarActionItemTemplate">
<Button Content="{Binding Header}"
Command="{Binding}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ToolBar}, Path=Tag.SelectedItems}"
Visibility="{Binding Visibility}" />
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.Children>
<ToolBar Grid.Row="0"
ItemsSource="{Binding Actions}"
ItemTemplate="{StaticResource toolBarActionItemTemplate}"
Tag="{Binding}" />
<ListView Grid.Row="1"
ItemsSource="{Binding Items}"
SelectionMode="Extended"
Tag="{Binding}">
<ListView.ContextMenu>
<ContextMenu ItemsSource="{Binding Actions}"
ItemContainerStyle="{StaticResource contextMenuItemStyle}" />
</ListView.ContextMenu>
<ListView.View>
<GridView>
<GridViewColumn Header="Id"
DisplayMemberBinding="{Binding Id}"/>
</GridView>
</ListView.View>
</ListView>
</Grid.Children>
</Grid>
</Window>
CommandBase.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Input;
namespace NullParameter
{
public abstract class CommandBase : ICommand, INotifyPropertyChanged
{
private Visibility _visibility;
private string _error;
public Visibility Visibility
{
get { return _visibility; }
protected set
{
if (_visibility == value)
return;
_visibility = value;
if (PropertyChanged == null)
return;
PropertyChanged(this, new PropertyChangedEventArgs("Visibility"));
}
}
public string Error
{
get { return _error; }
set
{
if (_error == value)
return;
_error = value;
if (PropertyChanged == null)
return;
PropertyChanged(this, new PropertyChangedEventArgs("Error"));
}
}
public bool CanExecute(object parameter)
{
Error = DoCanExecute(parameter);
Visibility = DoIsVisible(parameter) ? Visibility.Visible : Visibility.Collapsed;
return Error == null;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
DoExecute(parameter);
}
protected abstract string DoCanExecute(object parameter);
protected abstract bool DoIsVisible(object parameter);
protected abstract void DoExecute(object parameter);
public event PropertyChangedEventHandler PropertyChanged;
}
}
Command.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Input;
namespace NullParameter
{
public class Command : CommandBase
{
public Action OnExecute { get; set; }
public Func<bool> OnIsVisible { get; set; }
public Func<string> OnCanExecute { get; set; }
public string Header { get; set; }
protected override string DoCanExecute(object parameter)
{
if (OnCanExecute == null)
return null;
return OnCanExecute();
}
protected override bool DoIsVisible(object parameter)
{
if (OnIsVisible == null)
return true;
return OnIsVisible();
}
protected override void DoExecute(object parameter)
{
if (OnExecute == null)
return;
OnExecute();
}
}
public class Command<T> : CommandBase
where T : class
{
public Action<T> OnExecute { get; set; }
public Func<T, bool> OnIsVisible { get; set; }
public Func<T, string> OnCanExecute { get; set; }
public string Header { get; set; }
protected T Convert(object parameter)
{
if (parameter == null)
Console.WriteLine("NULL");
return parameter as T;
}
protected override string DoCanExecute(object parameter)
{
if (OnCanExecute == null)
return null;
var p = Convert(parameter);
if (p == null)
return "Invalid Parameter";
return OnCanExecute(p);
}
protected override bool DoIsVisible(object parameter)
{
if (OnIsVisible == null)
return true;
var p = Convert(parameter);
if (p == null)
return false;
return OnIsVisible(p);
}
protected override void DoExecute(object parameter)
{
if (OnExecute == null)
return;
var p = Convert(parameter);
if (p == null)
return;
OnExecute(p);
}
}
}
ListViewModel.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace NullParameter
{
public class ListViewModel
{
public IList<ViewModel> Items { get; private set; }
public IList SelectedItems { get; private set; }
public IList<ICommand> Actions { get; private set; }
public ListViewModel()
{
var items = new ObservableCollection<ViewModel>()
{
new ViewModel()
{
Id = 1
},
new ViewModel()
{
Id = 2
},
new ViewModel()
{
Id = 3
},
};
Items = items;
SelectedItems = items;
Actions = new List<ICommand>()
{
new Command()
{
OnExecute = ShowAlways,
Header = "Show Always"
},
new Command<IList<ViewModel>>()
{
OnExecute = ShowWhenSelected,
OnIsVisible = (list) => { return list.Count(o => o.IsSelected) > 0; },
Header = "Show When Selected"
}
};
}
public void ShowAlways()
{
Console.WriteLine("ShowAlways()");
}
public void ShowWhenSelected(IList<ViewModel> viewModels)
{
Console.WriteLine("ShowWhenSelected({0})", String.Join(",", viewModels.Where(o => o.IsSelected).Select(o => o.Id)));
}
}
}
ViewModel.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NullParameter
{
public class ViewModel : INotifyPropertyChanged
{
private bool _isSelected;
private int _id;
public event PropertyChangedEventHandler PropertyChanged;
public int Id
{
get { return _id; }
set
{
if (_id == value)
return;
_id = value;
if (PropertyChanged == null)
return;
PropertyChanged(this, new PropertyChangedEventArgs("Id"));
}
}
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected == value)
return;
_isSelected = value;
if (PropertyChanged == null)
return;
PropertyChanged(this, new PropertyChangedEventArgs("IsSelected"));
}
}
}
}
So after reading a few posts about how glitchy the CommandParameter DependencyProperty is, I've given up on using it entirely. Instead, I simply construct my Command objects by passing in the list of selected items in my ListViewModel. Then in the CanExecute and Execute method, I use the stored list of selected items instead of the .NET supplied parameter.
Although this provides a workable solution, it does not necessarily solve the problem posed by the initial question. So I will leave this here as a suggestion for anyone else unfortunate enough to have these issues.

Categories