I am doing some TPL in VS2012, WPF with MVVM. I have a question that I think I know the answer to but wanted to know for sure. Consider this snippet:
TaskCanceller = new CancellationTokenSource();
TaskLoader = Task<object>.Factory.StartNew(() =>
{
//Test the current query
DataRepository dr = new DataRepository(DataMappingSelected);
string test = dr.TestMappingConnection();
if (test.IsNotNullEmpty())
throw new DataConnectionException(test);
//Create the CSV File
DataQueryCsvFile CsvFile = new DataQueryCsvFile();
CsvFile.FileName = IO.Path.GetFileName(FilePath);
CsvFile.FilePath = IO.Path.GetDirectoryName(FilePath);
CsvFile.DataMapping = DataMappingSelected;
CsvFile.DataBrowserQuery = DataQueryHolder;
//Allow for updates to the UI
CsvFile.PageExportComplete += (s, e) =>
{
if (TaskCanceller.IsCancellationRequested)
(s as DataQueryCsvFile).IsSaveCancellationRequested = true;
StatusData = String.Format("{0} of {1} Complete", e.ProgressCount, e.TotalCount);
StatusProgress = (100 * e.ProgressCount / e.TotalCount);
};
CsvFile.SaveFile();
return CsvFile;
});
I have a class DataQueryCsvFile. Its intent is to create a CSV text file based off a passed set of query parameters the results of which can be very large. So the export "paginates" the table produced by the query so it does not blow the users memory. Among its members is an Event called PageExportComplete which is called whenever a "Page" is written to a file - say 1000 records at a time. The code below uses this event to update a progress indicator on the UI.
The progress indicators (StatusData and StatusProgress) are declared in the VM with appropriate Notification to let the View know when they are changed. For example:
public string StatusData
{
get { return _StatusData; }
set { NotifySetProperty(ref _StatusData, value, () => StatusData); }
}
private string _StatusData;
Here is my question - as is, this works very well. But why since I did NOT declare the Task to run or update via the UI thread (FromCurrentSynchronizationContext) in a ContinueWith.
Is it because the MVVM pattern? In other words, because the properties being updated are local to the VM and because they have the notification to update the View and because of the lose coupling via bindings its works? Or am I just lucky due to the circumstances and I should go through the trouble of declaring a ContinueWith to update progress on the UI thread?
UI related stuff can only be updated from UI thread whereas any CLR property binded to UI can be updated from background thread, they don't have thread affinity issue.
Like you posted in your sample, you are only updating View model properties from background thread which is perfectly fine but if you try updating Progress bar text directly, it will fall miserably since progressBar is UI component and can only be updated from UI thread.
Say you have TextBlock binded to Name property in ViewModel class:
<TextBlock x:Name="txt" Text="{Binding Name}"/>
If you try to update text directly, you will get famous thread affinity issue:
Task.Factory.StartNew(() => txt.Text = "From background");
But in case you try to update ViewModel Name property, it will work fine since no UI stuff is access from background thread here:
ViewModelClass vm = DataContext as ViewModelClass;
Task.Factory.StartNew(() => vm.Name = "From background");
Related
I want to show a network in my WPF-Application.
But always the window freeze on rendering.
I use the Graphsharp-library for visualization.
The network is assembled in an extra thread. So it will probably be the renders.
So, is it possible with WPF to load a part of the UI in the background?
My Code:
_graph = await Task.Run(()=> {
var g = new BidirectionalGraph<object, IEdge<object>>();
foreach (MyItem p in _myItemList)
{
g.AddVertex(p);
}
foreach (MyItem p in _myItemList)
{
foreach (MyItem n in p.Neighbors)
{
g.AddEdge(new Edge<object>(p, n));
}
}
return g;
});
OnPropertyChanged("Graph");
XAML:
xmlns:graphsharp="clr-namespace:GraphSharp.Controls;assembly=GraphSharp.Controls"
[...]
<graphsharp:GraphLayout
Graph="{Binding ElementName=root, Path=Graph}"
LayoutAlgorithmType="LinLog"
OverlapRemovalAlgorithmType="FSA"
HighlightAlgorithmType="Simple" />
NO operating system allows modifying the UI from another thread. In your case though you aren't trying to update the UI in the background, you are assembling the graph in the background and setting a property. There's no UI work here. If the rendering element binds to that property it will get the notification, read the property and update itself, all in the UI thread.
The code needs improvement though. Tasks aren't threads and there's no reason to use cold tasks and Task.Start() like that. Start() doesn't guarantee when a task will run. The task will still be scheduled for execution on the threadpool thread and possibly wait if there are no available threads there.
The code can be simplified to this :
var graph = await Task.Run(()=> {
var g = new BidirectionalGraph<object, IEdge<object>>();
foreach (MyItem p in _myItemList)
{
g.AddVertex(p);
}
foreach (MyItem p in _myItemList)
{
foreach (MyItem n in p.CallingList)
{
g.AddEdge(new Edge<object>(p, n));
}
}
return g;
};
_graph = graph;
OnPropertyChanged("Graph");
A better idea wold be to use a proper property instead of modifying fields and raising the event :
public BidirectionalGraph Graph
{
get => _graph;
set
{
_graph=value;
OnPropertyChanged(nameof(Graph));
}
}
...
public async Task MyGraphMethod()
{
Graph=graph;
}
UPDATE
The real question seems to be why does GraphLayout take so long to display the graph?
Set the AsyncCompute property of GraphLayout to true :
<graphsharp:GraphLayout
Graph="{Binding ElementName=root, Path=Graph}"
LayoutAlgorithmType="LinLog"
OverlapRemovalAlgorithmType="FSA"
HighlightAlgorithmType="Simple"
AsyncCompute = "true" />
GraphSharp was abandoned 6 years ago if not more. Codeplex itself shut down and now, the only available source code is either the archived source or forks on Github, like this one.
The examples on this repo show there is an AsyncCompute property that will run the layout algorithms in the background :
<sample:MyGraphLayout x:Name="Layout" LayoutAlgorithmType="ISOM" OverlapRemovalAlgorithmType="FSA" Graph="{Binding}"
AsyncCompute="true" ShowAllStates="false" HighlightAlgorithmType="Simple">
<sample:MyGraphLayout.LayoutParameters>
<isom:ISOMLayoutParameters Width="1200" Height="1200" />
</sample:MyGraphLayout.LayoutParameters>
</sample:MyGraphLayout>
When AsyncCompute is true, the Layout() method of the control uses a BackgroundWorker to perform the operation in the background
This property exists in the original projects source archive as well.
In WPF, using MVVM design, I've created a screen designed to load large numbers of logs into a ListView on the click of a button. Upon return, a label is updated to display the number of logs returned. This process can sometimes take a while. Our DA is in the process of optimizing things, but meanwhile I am required to make the following changes to indicate to the user that the search is running:
Display the mouse as a WaitCursor.
Update the label text to display "Searching...".
I have a class which implements ICommand and I have the WaitCursor working correctly. However, I cannot get the desired behavior for updating the label to display when the search is running. My current code:
MyScreen.xaml
<Button
Name="DisplayButton"
Content="Display Logs"
Command="{Binding DisplayLogsCommand}"
Margin="0,64,10,0"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Width="112"/>
<Label
Content="{Binding LogsShowingText}"
Margin="0,0,127,8"
Foreground="#FF3C3B3B"
HorizontalAlignment="Right"
Width="145" Height="24"
VerticalAlignment="Bottom"
HorizontalContentAlignment="Right"
FontSize="11"/>
MyScreenVM.cs
private Command displayLogsCommand;
private string logShowingText;
public Command DisplayLogsCommand
{
get
{
if (this.displayLogsCommand == null)
{
// Attempt 3 made here.
bool useWaitCursor = true;
Func<bool> canExecute = () => this.ValidateFields();
Action execute = () =>
{
/*
* TODO: Update this.LogsShowingText to read "Searching..."
*/
// Attempt 1 and 2 made here.
LogEntry[] entries = this.ClientConnection.GetLogs();
this.LogsShowingText = string.Format("Showing {0} Logs", entries.Length);
this.FilteredEntries = new ObservableCollection<LogEntry>(entries);
};
this.displayLogsCommand = new Command(useWaitCursor, canExecute, execute);
}
return this.displayLogsCommand;
}
}
public string LogsShowingText
{
get
{
return this.logsShowingText;
}
set
{
this.logsShowingText= value;
OnPropertyChanged("LogsShowingText");
}
}
Thus far the results and my associated failed attempts are below:
After logs are returned, the Label only reads "Searching...".
Application.Current.Dispatcher.Invoke(new Action(() => this.LogsShowingText = "Searching..."));
After logs are returned, the Label only reads "Showing N Logs".
this.LogsShowingText = string.Format("Searching...");
Before and during search, the Label reads "Searching...", then after logs are returned, Label reads "Showing N Logs". Same code as #2, different location.
I understand this probably has something to do with the UI being blocked until the Action completes, which clearly explains attempt 1 showing the last queued update to the label and attempt 2 showing the last hard coded update to the label. Attempt 3 almost works, but the Label should not be updated until the user has clicked the button to perform the search. How can I do this?
Since this is an expensive command, meaning the UI hangs while it processes, you should convert it to an async command.
public Command DisplayLogsCommand
{
get
{
if (this.displayLogsCommand == null)
{
bool useWaitCursor = true;
Func<bool> canExecute = () => this.ValidateFields();
Action execute = async () =>
{
this.LogsShowingText = "Searching";
// Attempt 1 and 2 made here.
LogEntry[] entries = await Task<LogEntry[]>.Run(() => this.ClientConnection.GetLogs());
this.LogsShowingText = string.Format("Showing {0} Logs", entries.Length);
this.FilteredEntries = new ObservableCollection<LogEntry>(entries);
};
this.displayLogsCommand = new Command(useWaitCursor, canExecute, execute);
}
return this.displayLogsCommand;
}
}
By making the Action delegate async, you can now await within the Action. This lets you wrap the call to your DataLayer in a Task and await it. Now that the expensive operation is running off the UI thread, your label will be updated properly before and after. No need to marshall the changes using a Dispatcher.
This will update the labels only when the command has been executed by the user clicking the button.
There are a lot of questions on here about updating controls(mainly the ObservableCollection) using a background worker thread. I am trying to implement this solution. However, my situation has just a little bit more depth to it because it calls to a function in the add method that returns the object to be added.
This is what my solution looks like right now (note that my ObservableCollection is bound to a TreeView control):
//Pass from Background Thread
MainTreeViewModel.AddLocation(locationName, locationValue);
//UI Thread (MainTreeViewModel)
public void AddLocation(MultiItemString displayName, int locationValue)
{
var node = Data.GetAllChildren(x => x.Children).Distinct().ToList().First(x => x.identify == 'P'); //Location Parent
App.Current.Dispatcher.BeginInvoke((Action)delegate()
{
node.Children.Add(CreateLocationNode(displayName, locationValue));
});
}
CreateLocationNode:
//Also location in MainTreeViewModel
private HierarchicalVM CreateLocationNode(MultiItemString displayName, int locationValue)
{
MultiItemString locationNumber = new MultiItemString(new[] {locationValue.ToString() + " "}, false);
var newLocationNode = new HierarchicalVM()
{
DisplayName = displayName, //Examples of props that turn out as NULL
LocationNumber = locationNumber,
TreeView_LocValue = locationValue,
Children = new ObservableCollection<HierarchicalVM>(), //This is what is being added to
Commands =
};
return newLocationNode;
}
When doing this I find that an object gets added, but all of the properties attached to it receive null values. Oppositely, when I am doing everything in the UI thread and just using node.Children.Add(CreateLocationNode(displayName, locationValue));, everything attaches how it should. Why am I getting a different result here, and how can I fix it?
Please let me know if you need more code.
I have problem with my GUI and Threads.
The GUI contains DataGrid. Every X time the program do some query and getting a list of items that I want to fill into the DataGrid.
So far so good:
private void loadTaskList() //Call every X time
{
List<myObject> myList = myquery();
this.Dispatcher.Invoke((Action)(() =>
{
TaskListTable.Items.Clear(); //Clear the DataGrid
foreach (myObject O in myList) //Add the items from the new query.
{
TaskListTable.Items.Add(O);
}
}));
FindSelectionObject(); // <-- see next explanation.
}
When the user click on one of the objects in the datagrid, the line color changed (it works fine), but when the program reload the table,The painted line disappears (Becuse I clear and add new objects).
To deal with it, I created the function FindSelectionObject():
private void FindSelectionObject()
{
this.Dispatcher.Invoke((Action)(() =>
{
this.SelectedIndex = TaskListTable.Items.IndexOf((myObject)lastSelectionObject); //find index of the new object that equels to the last selection object.
var row = TaskListTable.ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as DataGridRow; //get the row with the index
row.Background = Brushes.LightGoldenrodYellow; //repaint
}));
}
The problem: Everything works fine, but sometimes when the program reloads, the line flashes per second and then highlighted back, and sometimes it's not painting it at all (untill the next reload).
I can't understand why this is happening. I think maybe the FindSelectionObject() begins to run before the loadTaskList() ends to invoke all and add the new objects into the datagrid.
But if so - Why? And how can I fix it?
In the bottom line, I want that after every reload the line re-paint immediately..
Thanks for any advice!
A few things to think about:
You should keep in mind that the DataGrid uses virtualization, which means that each item in your items source does not get its very own UI element. The UI elements are created to fill the visible area, and then re-used depending on which data-source item is currently bound to each one (this changes when you scroll for instance or change the items source). This may cause you problems in the future if you use your current approach, so keep this in mind.
The other thing is that the DataGrid may require more "cycles" of the layout process in order to update its UI. You may simply be calling FindSelectionObject prematurely. You have queued FindSelectionObject right after the invocation in loadTaskList. If the DataGrid needs to perform some actions which are queued on the dispatcher after the items source has changed, these will execute after the invocation in FindSelectionObject.
Try this instead:
private void loadTaskList() //Call every X time
{
List<myObject> myList = myquery();
this.Dispatcher.Invoke((Action)(() =>
{
TaskListTable.Items.Clear(); //Clear the DataGrid
foreach (myObject O in myList) //Add the items from the new query.
{
TaskListTable.Items.Add(O);
}
// The items of the grid have changed, NOW we QUEUE the FindSelectionObject
// operation on the dispatcher.
FindSelectionObject(); // <-- (( MOVE IT HERE )) !!
}));
}
EDIT: OK, so if this fails then maybe this will cover the case in which the above solution fails: subscribe to the LoadingRow event of DataGrid and set the appropriate background color if the row is the selected one. So in the cases when new rows are created this event will be called (due to virtualization it is not called per item in items source, but per actual row UI element). In the event args you will have access to the created DataGridRow instance.
I think this issue could be a visual thread synchronization. For this you can create and use a method similar like this:
public void LockAndDoInBackground(Action action, string text, Action beforeVisualAction = null, Action afterVisualAction = null)
{
var currentSyncContext = SynchronizationContext.Current;
var backgroundWorker = new BackgroundWorker();
backgroundWorker.DoWork += (_, __) =>
{
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");
Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US");
currentSyncContext.Send((t) =>
{
IsBusy = true;
BusyText = string.IsNullOrEmpty(text) ? "Espere por favor..." : text;
if (beforeVisualAction != null)
beforeVisualAction();
}, null);
action();
currentSyncContext.Send((t) =>
{
IsBusy = false;
BusyText = "";
if (afterVisualAction != null)
afterVisualAction();
}, null);
};
backgroundWorker.RunWorkerAsync();
}
IsBusy and BusyText are particular properties, that you can remove. The action variable will be the action to do in background (load your items for instance). beforeVisualAction and afterVisualAction are the visual actions you may want to do before and after the background action. Here are any visual update, for instance select your item, change color, set a view model variable that raise a binding update,... (any action that update the view).
Hope this method helps.
Are you maintaining the reference to lastSelectionObject somewhere? You say you're adding new objects, if they are truly new then the reference will be different and the reference comparison happening in IndexOf will not find it.
This is a multi threaded scenario.
The main thread handles the application and UI events, and it starts up a new thread to do some background operations.
The "background" thread loads the data from files into a data-table of a strongly-typed dataset. The DataGridView is bound to that DataTable.
Once the data is ready, the "background" thread invokes the refresh() function of the DataGridView on the form.
If there are more lines then what fits on one screen and the vertical scrollbar is to appear: the grid crashes. The new datalines are always displayed. Error only occurs if there are enough lines to display the scrollbar (see image below).
I use .NET 3.5. In Windows XP it crashes the whole application. On Win 7 (64 bit) only the grid becomes unresponsive, but once I resize the window the scrollbar appears and all is fine.
The relevant parts of the code are attached below.
Grid refresh operation in the form's .cs file:
public void ThreadSafeRebindGrids()
{
SimpleCallBack callBackHandler = new SimpleCallBack(RebindGrids);
this.BeginInvoke(callBackHandler);
}
public void RebindGrids()
{
gridCurrentResults.Refresh(); // The problematic DataGridView refresh()
gridAllResults.Refresh();
}
public delegate void SimpleCallBack();
The update part in the "background" thread:
void Maestro32_SampleFinished(object sender, MeasurementEvents.SampleFinishedEventArgs e)
{
//--- Read new results
ParentForm.ThreadSafeSetStatusInfo("Processing results for sample no. " + e.SampleNo.ToString() + "...");
CurrentMeasurement.ReadSpeResults(); // Updating the DataTable in the strongly typed DataSet (see below)
ParentForm.ThreadSafeRebindGrids(); // Refresh the DataGridView
ParentForm.ThreadSafeRefreshNumbers();
}
The objects related to the "background" thread have a direct reference to the DataSet (UiDataSource). The DataTable (CurrentSamples) is updated in the following manner:
/// <summary>
/// Adds a new sample to the CurrentSamples table of the UiDataSet.
/// </summary>
/// <param name="sample">The new sample to be added to the table.</param>
/// <param name="serial">The serial number of the sample being added</param>
private void AddSampleToCurrentResults(SampleData sample, int serial)
{
UiDataSource.CurrentSamples.AddCurrentSamplesRow(serial,
sample.MeasurementDate,
(uint)Math.Round(sample.SampleCountSum),
true, //--- Set the checkbox checked
sample.LiveTime,
sample.RealTime);
}
DataGridView options:
//
// gridCurrentResults (generated)
//
this.gridCurrentResults.AllowUserToAddRows = false;
this.gridCurrentResults.AllowUserToDeleteRows = false;
this.gridCurrentResults.AllowUserToOrderColumns = true;
this.gridCurrentResults.AllowUserToResizeRows = false;
this.gridCurrentResults.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.gridCurrentResults.AutoGenerateColumns = false;
this.gridCurrentResults.CausesValidation = false;
this.gridCurrentResults.ColumnHeadersHeight = 25;
this.gridCurrentResults.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.selectedCol,
this.SampleNoCol,
this.MeasuredValueCol,
this.liveTimeCol,
this.realTimeDataGridViewTextBoxColumn,
this.AtTimeCol});
this.gridCurrentResults.DataMember = "CurrentSamples";
this.gridCurrentResults.DataSource = this.uiDataSource;
this.gridCurrentResults.Location = new System.Drawing.Point(11, 24);
this.gridCurrentResults.Margin = new System.Windows.Forms.Padding(8);
this.gridCurrentResults.Name = "gridCurrentResults";
this.gridCurrentResults.RowHeadersVisible = false;
this.gridCurrentResults.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
this.gridCurrentResults.ShowEditingIcon = false;
this.gridCurrentResults.Size = new System.Drawing.Size(534, 264);
this.gridCurrentResults.TabIndex = 0;
this.gridCurrentResults.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.gridCurrentResults_CellContentClick);
If I made a mistake somewhere please point it out to me.
#ChrisF:
I tried removing the refresh() statement, as I am doing pretty much the same what u suggested. The only difference is the databinding, it looks like:
this.dataGridView.DataSource = this.dataSet;
this.dataGridView.DataMember = "dataTable";
And I update the dataTable in a similar way, but from another thread.
But the new data lines do not appear until I, say, resize the window.
Which raises the question how I can properly update the dataTable from another thread?
I'm guessing the problem has to do with how WinForms works inside the STA model for threading. Basically, the DataTable you're accessing is located somewhere, and that is probably inside the form we see above. So, when you update the DataTable from another thread, which thread gets the events needed for binding? Likely the thread you update it from, and the form's thread is not aware of the changes being made. So, you simply need to invoke any calls to DataTable onto the form itself, so it receives the events properly:
this.Invoke(() => {
// any calls involving DataTable
});
It seems backwards, but keep in mind in an "enterprise" situation, you'd probably be accessing that dataset by multiple adapters. So, your update thread would have an adapter to itself, and your GUI would have its own also. The other solution would be to use a BindingList, which I believe has thread compatibility for this type of situation, but don't quote me on that.
For extra credit, this could also explain your problem before with crashing. By accessing the DataGridView from the background thread, you had cross-thread operations going on.
I wouldn't call:
gridCurrentResults.Refresh(); // The problematic DataGridView refresh()
gridAllResults.Refresh();
These will take progressively longer and longer as the data set gets larger and larger.
I've written an application that uses a DataGridView to display mp3 file information. I set the DataSource of the DataGridView to a DataTable:
this.dataGridView.DataSource = this.dataTable;
and then simply add the new information to the DataTable:
this.dataTable.Rows.Add(row);
This automatically updates the DataGridView.