I am building an app, where I will watching the files. I am pretty fresh in programming so I (maybe) have little issue, because I dont have some real practice. My program its working, but I dont know how is usally implemented in "real" software. Please overview and comment. Because I like to have much as is possible in DateFiles class and less is possible in MainViewModel.
Yes app in build on MVVM base.
My Current state:
In my MainViewModel
public static string ConfigurationFilesSourcePath2;
private void InitializeFiles()
{
// New instance of DateFiles
DF = new DataFiles();
// Path to DateFiles
DF.ConfigurationFilesSourcePath = ConfigurationFilesSourcePath;
// Run Initialization method to establish "filewatching"
DF.InitializeFiles();
// Refresh ListView in View
RefreshFileList();
// Assign a Handler to PropertyChanged event
DF.PropertyChanged += DF_PropertyChanged;
}
// If something change inside DateFiles
private void DF_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
RefreshFileList();
}
// Refresh file list
public void RefreshFileList()
{
fileslist = new ObservableCollection<Files>();
foreach (var item in DF.fileslist)
{
fileslist.Add(item);
}
NotifyPropertyChanged("fileslist");
}
DataFiles class:
public class DataFiles : ViewModelBase
{
public FileSystemWatcher filewatcher;
public string ConfigurationFilesSourcePath;
public ObservableCollection<Files> fileslist { get; set; } = new ObservableCollection<Files>();
public void InitializeFiles()
{
// Create a new FileSystemWatcher
filewatcher = new FileSystemWatcher();
// Set filter to only catch XAL files
filewatcher.Filter = "*.txt";
// Set the path
filewatcher.Path = ConfigurationFilesSourcePath;
// Subscribe to the Created event
filewatcher.Created += new FileSystemEventHandler(FileOnchanged);
filewatcher.Changed += new FileSystemEventHandler(FileOnchanged);
filewatcher.Deleted += new FileSystemEventHandler(FileOnchanged);
filewatcher.Renamed += new RenamedEventHandler(FileOnRenamed);
// Enable the FileSystemWatcher events
filewatcher.EnableRaisingEvents = true;
RefreshFilesList();
}
private void FileOnchanged(object sender, FileSystemEventArgs e)
{
RefreshFilesList();
}
private void FileOnRenamed(object sender, RenamedEventArgs e)
{
RefreshFilesList();
}
public void RefreshFilesList()
{
fileslist.Clear();
DirectoryInfo dir = new DirectoryInfo(ConfigurationFilesSourcePath);
string[] extensions = new[] { ".txt" };
int nof = 0;
foreach (FileInfo file in dir.GetFiles().Where(f => extensions.Contains(f.Extension.ToLower())).ToArray())
{
nof++;
fileslist.Add(new Files()
{
FileId = nof,
FileName = file.Name,
FileChanged = file.LastWriteTime.ToString(),
FileCreated = file.CreationTime.ToString(),
OnlyNameWithoutExtension = Path.GetFileNameWithoutExtension(file.Name)
});
NotifyPropertyChanged("fileslist");
}
}
There are several issues with your code. Among most important ones - you rebuild whole file list on every change (while FileSystemEventArgs arguments provide you with info about what has really changed and where) and you don't update your ObservableCollection on UI thread. Look at the following code (but keep in mind that is just a sample for you to spot some problems):
public class DataFiles : IDisposable {
public FileSystemWatcher filewatcher;
private readonly object fileListLock = new object();
// you don't need public setter on this
public ObservableCollection<Files> Fileslist { get; } = new ObservableCollection<Files>();
// pass path here, no need to use property
public void InitializeFiles(string path) {
// dispose existing watcher, if any
DisposeWatcher();
// Create a new FileSystemWatcher
filewatcher = new FileSystemWatcher();
// Set filter to only catch XAL files
filewatcher.Filter = "*.txt";
// Set the path
filewatcher.Path = path;
// Subscribe to the Created event
filewatcher.Created += new FileSystemEventHandler(FileOnchanged);
filewatcher.Changed += new FileSystemEventHandler(FileOnchanged);
filewatcher.Deleted += new FileSystemEventHandler(FileOnchanged);
filewatcher.Renamed += new RenamedEventHandler(FileOnRenamed);
// don't RefreshFilesList on UI thread, that might take some time and will block UI
Task.Run(() => RefreshFilesList());
// Enable the FileSystemWatcher events
filewatcher.EnableRaisingEvents = true;
}
private void FileOnchanged(object sender, FileSystemEventArgs e) {
// lock here to avoid race conditions with RefreshFilesList
lock (fileListLock) {
// better use dictionary to avoid looping over all files
// but looping is still much better than rebuilding whole list
var file = Fileslist.FirstOrDefault(c => String.Equals(c.FullPath, e.FullPath, StringComparison.OrdinalIgnoreCase));
if (file != null) {
if (e.ChangeType == WatcherChangeTypes.Deleted)
; // delete
else
; // update file properties
}
else {
// add new, unless event is delete
}
}
}
private void FileOnRenamed(object sender, RenamedEventArgs e) {
lock (fileListLock) {
// better use dictionary to avoid looping over all files
var file = Fileslist.FirstOrDefault(c => String.Equals(c.FullPath, e.OldFullPath, StringComparison.OrdinalIgnoreCase));
if (file != null) {
file.FullPath = e.FullPath;
}
else {
// add new
}
}
}
public void RefreshFilesList() {
// you need to lock here, because there is a race condition between this method and FileOnRenamed \ FileOnChanged,
// and you might lose some updates or get duplicates.
lock (fileListLock) {
// update ObservableCollection on UI thread
OnUIThreadDo(() => {
Fileslist.Clear();
});
DirectoryInfo dir = new DirectoryInfo(filewatcher.Path);
int nof = 0;
var files = new List<Files>();
// just use EnumerateFiles
foreach (FileInfo file in dir.EnumerateFiles("*.txt")) {
nof++;
int tmp = nof;
// if you are working with UI (that is most likely the case if you use ObservableCollection) -
// you need to update that collection from UI thread if you have bound controls
files.Add(new Files() {
FileId = tmp,
FullPath = file.FullName,
FileChanged = file.LastWriteTime,
FileCreated = file.CreationTime,
});
// don't do that
// NotifyPropertyChanged("fileslist");
}
// publish them all to collection on UI thread
OnUIThreadDo(() => {
foreach (var file in files)
Fileslist.Add(file);
});
}
}
private void OnUIThreadDo(Action a) {
if (Application.Current.CheckAccess())
a();
else
Application.Current.Dispatcher.BeginInvoke(a);
}
public void Dispose() {
DisposeWatcher();
}
private void DisposeWatcher() {
if (filewatcher != null) {
filewatcher.EnableRaisingEvents = false;
filewatcher.Created -= FileOnchanged;
filewatcher.Deleted -= FileOnchanged;
filewatcher.Changed -= FileOnchanged;
filewatcher.Renamed -= FileOnRenamed;
filewatcher.Dispose();
}
}
}
public class Files : INotifyPropertyChanged
{ // implement INotifyPropertyChanged, because you need to reflect property changes in UI
public int FileId { get; set; }
public string FullPath { get; set; }
public string FileName => Path.GetFileName(FullPath);
public DateTime FileChanged { get; set; }
public DateTime FileCreated { get; set; }
public string OnlyNameWithoutExtension => Path.GetFileNameWithoutExtension(FullPath);
}
Related
I have a windows service that basically monitors a folder, copies files to a local directory processes the files. The service starts up fine enough calls the Watcher Class and does all of the configuration like it should. So my code works for the setup. However I get 0 events firing. I am wondering if when my StartFolderWatcher() method goes out of scope that my object - even though declared at the class level is somehow getting disposed.
So do I need to run the object continuously in a separate thread ? If so can I get an example of how I should do that Task.Run(()=> myObject)
Relevant Code is below - if more is needed just ask and I will post it.
static class Program
{
static void Main(string[] args)
{
JobProcessor service = new JobProcessor();
if (Environment.UserInteractive)
{
service.RunAsConsole(args);
}
}
private static bool IsDebugMode(string[] args)
{
if (args == null) return false;
if (args[0].ToLower() == "/debug") return true;
return false;
}
}
public partial class JobProcessor : ServiceBase
{
string[] folders = new string[] {"InBoxFolder"};
HotFolderWatch HFW = new HotFolderWatch();
public JobProcessor()
{
InitializeComponent();
}
protected override void OnStart(string[] args)
{
StartFolderWatcher();
}
public void StartFolderWatcher()
{
FileWatcherInfo[] ServiceWatchers = new FileWatcherInfo[4];
ServiceConfiguration sc = new ServiceConfiguration();
for (int i = 0; i < folders.Length; i++)
{
ServiceWatchers[i] = sc.GetWatchFolderSettings(folders[i]);
}
HFW = new HotFolderWatch(ServiceWatchers[0]);
HFW.ReadyToProcess += ReadyToProcess;
HFW.InBoxFolderDisconnected += OnInBoxFolderDisconnected;
HFW.LocalFolderDisconnected += OnLocalFolderDisconnected;
HFW.ProcessFolderDisconnected += OnProcessFolderDisconnected;
}
public void RunAsConsole(string[] args)
{
OnStart(args);
Console.WriteLine("Press any key to exit...");
Console.ReadLine();
OnStop();
}
}
public HotFolderWatch(FileWatcherInfo inbox)
{
this.InboxCacheTimeMilliseconds = inbox.CacheTimeMilliseconds;
this.InBoxFolder = inbox.Folder.Trim();
this.InboxFileFilter = inbox.Filter.Trim();
SetInboxWatcher();
}
private void SetInboxWatcher()
{
InBoxWatcher = new FileSystemWatcher(InBoxFolder, InboxFileFilter);
InBoxWatcher.IncludeSubdirectories = false;
InBoxWatcher.NotifyFilter =
NotifyFilters.LastAccess | NotifyFilters.LastWrite;
InboxCache = MemoryCache.Default;
InboxCachePolicy = new CacheItemPolicy()
{
RemovedCallback = OnRemovedFromInBoxCache
};
InBoxWatcher.Created += new FileSystemEventHandler(OnInBoxChanged);
InBoxWatcher.EnableRaisingEvents = true;
}
private void OnInBoxChanged(object source, FileSystemEventArgs e)
{
InboxCachePolicy.AbsoluteExpiration =
DateTimeOffset.Now.AddMilliseconds(InboxCacheTimeMilliseconds);
InboxCache.AddOrGetExisting(e.Name, e, InboxCachePolicy);
}
}
If in my HotFolderWatch Class I create a FileSystemWatcher in a method as below the code works!
private void CreateWatcher()
{
FileSystemWatcher fsw = new FileSystemWatcher(#"C:\Tests\JobQueue\InFolder","*.txt");
fsw.Created += Fsw_Created;
fsw.EnableRaisingEvents = true;
}
private void Fsw_Created(object sender, FileSystemEventArgs e)
{
string ex = e.FullPath;
WatcherChangeTypes ctmp = e.ChangeType;
// throw new NotImplementedException();
}
Turns out the default constructor of FileSystemWatcher uses the filter FileName by default and I was thinking I would get a last Write or Last Access with out concerning myself with the Name.
I received good information from here :
https://www.codeproject.com/Articles/1220094/NotifyFilters-Enumeration-Explained-FileSystemWatc
The article posted in the link is what helped me to solve my issue.
I had to Ste the NotifyFilters.FileName as one of the filters.
I have a windows service implemented in C#. The service watches a number of directories for a file to be saved. However to make it more robust I am trying to get it to check the folders after a period of time.
The inner class called a Watcher initialises both the fileSystemWatcher and DispatchTimer.
public class Watcher
{
FileSystemWatcher fileSystemWatcher = null;
public delegate void TimerInvokedHandler(object sender);
public event TimerInvokedHandler TimerInvoked;
public DispatcherTimer Timer { get; set; }
public int TimerMinutes { get; set; }
public Watcher()
{
}
public Watcher(String directory, String filter, String dap)
{
this.DAP = dap;
this.Directory = directory;
this.Filter = filter;
}
public Boolean EnableRaisingTimerEvents
{
get { return this.Timer.IsEnabled; }
set
{
this.Timer.IsEnabled = value;
if (value)
{
this.Timer.Start();
Log("Timer Started");
}
else
{
this.Timer.Stop();
Log("Timer Stopped");
}
}
}
private void Timer_Tick(object sender, EventArgs e)
{
StopWatch();
TimerInvoked?.Invoke(sender);
}
public void StartWatch()
{
if (fileSystemWatcher == null)
{
fileSystemWatcher = new FileSystemWatcher();
fileSystemWatcher.Filter = Filter;
fileSystemWatcher.Path = Directory;
fileSystemWatcher.Created += FileSystemWatcher_Created;
fileSystemWatcher.Renamed += FileSystemWatcher_Renamed;
fileSystemWatcher.Error += FileSystemWatcher_Error;
}
if (this.Timer == null)
{
Log("Initialising Timer");
this.Timer = new DispatcherTimer();
this.Timer.Interval = new TimeSpan(0, this.TimerMinutes, 0);
this.Timer.Tick += Timer_Tick;
}
this.EnableRaisingFileSystemsEvents = true;
this.EnableRaisingTimerEvents = true;
Log(String.Format("Watching Directory {0}", Directory));
}
// stops timer and file system events
public void StopWatch()
{
if (fileSystemWatcher != null)
EnableRaisingFileSystemsEvents = false;
if (Timer != null)
{
EnableRaisingTimerEvents = false;
}
Log(String.Format("Watching {0} Switched Off", this.Directory));
}
private void Log ( String Message )
{
LogEvent?.Invoke(this, Message);
}
}
The outer class creates a list of these watchers, this is based on ServiceBase, as in it is a service and so only ends when it gets a stopped within the Windows Service Manager
foreach (nhs_acquisition_profile pfl in p)
{
Watcher w = null;
String filePattern = String.Empty;
try
{
profileWatcherLog.WriteEntry(String.Format("Attempting to set-up watcher on {0} for DAP {1}",pfl.dap_file_location,pfl.dap_name));
if (pfl.dap_acquisition_method_loca.ToLower() == "xml") w = new Watcher(pfl.dap_file_location, "*.xml", pfl.dap_name);
else w = new Watcher(pfl.dap_file_location, "*.*", pfl.dap_name);
profileWatcherLog.WriteEntry("Initialising Event Handlers");
// initialise event handlers
w.FileCreated += W_FileCreated;
w.FileRenamed += W_FileRenamed;
w.TimerInvoked += W_TimerInvoked;
w.LogEvent += W_LogEvent;
profileWatcherLog.WriteEntry("Event Handlers initialised");
// dispatch timer
w.TimerMinutes = Convert.ToInt32(Properties.Settings.Default.TimerDelay);
w.StartWatch();
profileWatcherLog.WriteEntry("Watch started....Adding to Watcher List");
// add the watcher to the list of watchers
FileWatcherList.Add(w);
profileWatcherLog.WriteEntry("Added to list of file watchers");
profileWatcherLog.WriteEntry(String.Format("Watching {0} for files matching *.* for DAP {1}",pfl.dap_file_location,pfl.dap_name));
}
catch
{
throw;
}
}
The variable FileWatcherList is a field of the ProfileWatcher class which forms the service as a whole.
What I am finding is that the DispatchTimer Tick event never happens. I'm reasonable certain that this is not an instance of the DispatchTimer being disposed of before the Tick, but can't see why it is not firing.
Dmitri is correct, I should have been using a Timer and not a DispatchTimer. Once I moved to that it worked fine.
I don't know if my title is correct. But here's what I want to know.
I have a Download class that returns certain events and has a couple of methods. Each instance of Download class can download a single file. And all those events and methods are related to the file being downloaded.
As it's a multi file downloader, multiple instantiations are required when more than a single file needs to be downloaded.
Each download has a download id, but that is not supplied to the Download class, to keep it independent from the other classes.
Now getting all the info from each instance of the file download and being able to control a single download, is the problem. How do I know which download is which?
Any solutions? Or design patterns you could recommend? I've hit a roadblock.
Download class:
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Threading;
namespace Mackerel_Download_Manager
{
public class Download
{
public event EventHandler<DownloadStatusChangedEventArgs> ResumablityChanged;
public event EventHandler<DownloadProgressChangedEventArgs> ProgressChanged;
public event EventHandler Completed;
public bool stop = true; // by default stop is true
public bool paused = false;
SemaphoreSlim pauseLock = new SemaphoreSlim(1);
string filename;
public void DownloadFile(Uri DownloadLink, string Path)
{
filename = System.IO.Path.GetFileName(Path);
stop = false; // always set this bool to false, everytime this method is called
var fileInfo = new FileInfo(Path);
long existingLength = 0;
if (fileInfo.Exists)
existingLength = fileInfo.Length;
var request = (HttpWebRequest)HttpWebRequest.Create(DownloadLink);
request.Proxy = null;
request.AddRange(existingLength);
try
{
using (var response = (HttpWebResponse)request.GetResponse())
{
long fileSize = existingLength + response.ContentLength; //response.ContentLength gives me the size that is remaining to be downloaded
bool downloadResumable; // need it for not sending any progress
if ((int)response.StatusCode == 206) //same as: response.StatusCode == HttpStatusCode.PartialContent
{
//Console.WriteLine("Resumable");
downloadResumable = true;
}
else // sometimes a server that supports partial content will lose its ability to send partial content(weird behavior) and thus the download will lose its resumability
{
if (existingLength > 0)
{
if (ResumeUnsupportedWarning() == false) // warn and ask for confirmation to continue if the half downloaded file is unresumable
{
return;
}
}
existingLength = 0;
downloadResumable = false;
}
OnResumabilityChanged(new DownloadStatusChangedEventArgs(downloadResumable));
using (var saveFileStream = fileInfo.Open(downloadResumable ? FileMode.Append : FileMode.Create, FileAccess.Write))
using (var stream = response.GetResponseStream())
{
byte[] downBuffer = new byte[4096];
int byteSize = 0;
long totalReceived = byteSize + existingLength;
var sw = Stopwatch.StartNew();
while (!stop && (byteSize = stream.Read(downBuffer, 0, downBuffer.Length)) > 0)
{
saveFileStream.Write(downBuffer, 0, byteSize);
totalReceived += byteSize;
float currentSpeed = totalReceived / (float)sw.Elapsed.TotalSeconds;
OnProgressChanged(new DownloadProgressChangedEventArgs(totalReceived, fileSize, (long)currentSpeed));
pauseLock.Wait();
pauseLock.Release();
}
sw.Stop();
}
}
if (!stop) OnCompleted(EventArgs.Empty);
}
catch (WebException e)
{
System.Windows.MessageBox.Show(e.Message, filename);
}
}
public void pause()
{
if (!paused)
{
paused = true;
// Note this cannot block for more than a moment
// since the download thread doesn't keep the lock held
pauseLock.Wait();
}
}
public void unpause()
{
if (paused)
{
paused = false;
pauseLock.Release();
}
}
public void StopDownload()
{
stop = true;
this.unpause(); // stop waiting on lock if needed
}
public bool ResumeUnsupportedWarning()
{
var messageBoxResult = System.Windows.MessageBox.Show("When trying to resume the download , Mackerel got a response from the server that it doesn't support resuming the download. It's possible that it's a temporary error of the server, and you will be able to resume the file at a later time, but at this time Mackerel can download this file from the beginning.\n\nDo you want to download this file from the beginning?", filename, System.Windows.MessageBoxButton.YesNo);
if (messageBoxResult == System.Windows.MessageBoxResult.Yes)
{
return true;
}
else
{
return false;
}
}
protected virtual void OnResumabilityChanged(DownloadStatusChangedEventArgs e)
{
var handler = ResumablityChanged;
if (handler != null)
{
handler(this, e);
}
}
protected virtual void OnProgressChanged(DownloadProgressChangedEventArgs e)
{
var handler = ProgressChanged;
if (handler != null)
{
handler(this, e);
}
}
protected virtual void OnCompleted(EventArgs e)
{
var handler = Completed;
if (handler != null)
{
handler(this, e);
}
}
}
public class DownloadStatusChangedEventArgs : EventArgs
{
public DownloadStatusChangedEventArgs(bool canResume)
{
ResumeSupported = canResume;
}
public bool ResumeSupported { get; private set; }
}
public class DownloadProgressChangedEventArgs : EventArgs
{
public DownloadProgressChangedEventArgs(long totalReceived, long fileSize, long currentSpeed)
{
BytesReceived = totalReceived;
TotalBytesToReceive = fileSize;
CurrentSpeed = currentSpeed;
}
public long BytesReceived { get; private set; }
public long TotalBytesToReceive { get; private set; }
public float ProgressPercentage
{
get
{
return ((float)BytesReceived / (float)TotalBytesToReceive) * 100;
}
}
public float CurrentSpeed { get; private set; } // in bytes
public TimeSpan TimeLeft
{
get
{
var bytesRemainingtoBeReceived = TotalBytesToReceive - BytesReceived;
return TimeSpan.FromSeconds(bytesRemainingtoBeReceived / CurrentSpeed);
}
}
}
}
Download class is instantiated inside a Mackerel class, that starts the download for the given downloads.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows;
namespace Mackerel_Download_Manager
{
public static class Mackerel
{
//Main Menu functions
public static void ResumeDownload(string[] DownloadIDs)
{
foreach (var DownloadID in DownloadIDs)
{
var itemToResume = Downloads.DownloadEntries.Where(download => download.DownloadID == DownloadID).FirstOrDefault();
if (itemToResume.Running == false)
{
itemToResume.Running = true;
var download = new Download();
download.DownloadFile(itemToResume.DownloadLink, itemToResume.SaveTo);
var window = new Dialogs.DownloadProgress(itemToResume);
window.Show();
double progress = 0;
itemToResume.Status = string.Format("{0:0.00}%", progress);
Downloads.DownloadEntries.CollectionChanged += delegate
{
if (!itemToResume.Running) window.Close();
};
}
}
}
public static void StopDownload(string[] DownloadIDs)
{
foreach (var DownloadID in DownloadIDs)
{
var itemToStop = Downloads.DownloadEntries.Where(download => download.DownloadID == DownloadID).FirstOrDefault();
if (itemToStop.Running == true)
itemToStop.Running = false;
}
}
public static void StopAllDownloads()
{
foreach (var itemToStop in Downloads.DownloadEntries.Where(download => download.Running == true))
itemToStop.Running = false;
}
public static void RemoveDownload(string[] DownloadIDs) // this method is able to delete multiple downloads
{
foreach (var DownloadID in DownloadIDs)
{
// delete from the download list
var selectedDownload = Downloads.DownloadEntries.Where(download => download.DownloadID == DownloadID).FirstOrDefault();
var selectedDownloadIndex = Downloads.DownloadEntries.IndexOf(selectedDownload);
Downloads.DownloadEntries.RemoveAt(selectedDownloadIndex);
//delete from the harddrive
if (File.Exists(selectedDownload.SaveTo))
File.Delete(selectedDownload.SaveTo);
}
Downloads.Serialize(); // save current state of object
}
public static void RemoveCompletedDownloads() // this method just removes all completed downloads from Mackerel's download list (it doesn't delete them from the hard drive)
{
foreach (var itemToRemove in Downloads.DownloadEntries.Where(download => download.Status == "Complete").ToList())
{
Downloads.DownloadEntries.Remove(itemToRemove);
}
}
// Context Menu
public static void OpenDownloadProperties(string DownloadID) // Open "Download Properties" for the given download ID
{
var DownloadProperties = new Dialogs.Context_Menu.DownloadProperties(DownloadID);
DownloadProperties.Owner = Application.Current.MainWindow; // so that this dialog centers to its parent window, as its window is set to WindowStartupLocation="CenterOwner"
DownloadProperties.ShowDialog();
}
}
}
Full source code is here: https://github.com/Expenzor/mackerel-download-manager
Sounds like your making the download itself an object but are using "downloader" as the name instead. I would maybe suggest an array of downloaded objects or similar. The downloader class can have a method which creates a new object - perhaps a download object. Using an array or linked list etc will give you the opportunity to address the object and call them independently - know which is which.
Posting your code would help as well.
in my WPF - C# application, I have a time consuming function, which I execute with a BackgroundWorker. The job of this function is to add given data from a file into a database. Now and then, I need some user feedback, for example the data is already in the store and I want to ask the user, whether he wants to merge the data or create a new object or skip the data completely. Much like the dialog windows shows, if I try to copy a file to a location, where a file with the same name already exists.
The problem is, that I cannot call a GUI-window from a non GUI-thread. How could I implement this behavior?
Thanks in advance,
Frank
You could work with EventWaitHandle ou AutoResetEvent, then whenever you want to prompt the user, you could the signal UI, and then wait for the responde. The information about the file could be stored on a variable.
If possible... my suggestion is to architect your long running task into atomic operations. Then you can create a queue of items accessible by both your background thread and UI thread.
public class WorkItem<T>
{
public T Data { get; set; }
public Func<bool> Validate { get; set; }
public Func<T, bool> Action { get; set; }
}
You can use something like this class. It uses a queue to manage the execution of your work items, and an observable collection to signal the UI:
public class TaskRunner<T>
{
private readonly Queue<WorkItem<T>> _queue;
public ObservableCollection<WorkItem<T>> NeedsAttention { get; private set; }
public bool WorkRemaining
{
get { return NeedsAttention.Count > 0 && _queue.Count > 0; }
}
public TaskRunner(IEnumerable<WorkItem<T>> items)
{
_queue = new Queue<WorkItem<T>>(items);
NeedsAttention = new ObservableCollection<WorkItem<T>>();
}
public event EventHandler WorkCompleted;
public void LongRunningTask()
{
while (WorkRemaining)
{
if (_queue.Any())
{
var workItem = _queue.Dequeue();
if (workItem.Validate())
{
workItem.Action(workItem.Data);
}
else
{
NeedsAttention.Add(workItem);
}
}
else
{
Thread.Sleep(500); // check if the queue has items every 500ms
}
}
var completedEvent = WorkCompleted;
if (completedEvent != null)
{
completedEvent(this, EventArgs.Empty);
}
}
public void Queue(WorkItem<T> item)
{
// TODO remove the item from the NeedsAttention collection
_queue.Enqueue(item);
}
}
Your UI codebehind could look something like
public class TaskRunnerPage : Page
{
private TaskRunner<XElement> _taskrunner;
public void DoWork()
{
var work = Enumerable.Empty<WorkItem<XElement>>(); // TODO create your workItems
_taskrunner = new TaskRunner<XElement>(work);
_taskrunner.NeedsAttention.CollectionChanged += OnItemNeedsAttention;
Task.Run(() => _taskrunner.LongRunningTask()); // run this on a non-UI thread
}
private void OnItemNeedsAttention(object sender, NotifyCollectionChangedEventArgs e)
{
// e.NewItems contains items that need attention.
foreach (var item in e.NewItems)
{
var workItem = (WorkItem<XElement>) item;
// do something with workItem
PromptUser();
}
}
/// <summary>
/// TODO Use this callback from your UI
/// </summary>
private void OnUserAction()
{
// TODO create a new workItem with your changed parameters
var workItem = new WorkItem<XElement>();
_taskrunner.Queue(workItem);
}
}
This code is untested! But the basic principle should work for you.
Specifically to your case
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
Thread.Sleep(1000);
var a = Test1("a");
Thread.Sleep(1000);
var b = (string)Invoke(new Func<string>(() => Test2("b")));
MessageBox.Show(a + b);
}
private string Test1(string text)
{
if (this.InvokeRequired)
return (string)this.Invoke(new Func<string>(() => Test1(text)));
else
{
MessageBox.Show(text);
return "test1";
}
}
private string Test2(string text)
{
MessageBox.Show(text);
return "test2";
}
Test2 is a normal method which you have to invoke from background worker. Test1 can be called directly and uses safe pattern to invoke itself.
MessageBox.Show is similar to yourForm.ShowDialog (both are modal), you pass parameters to it (text) and you return value (can be a value of property of yourForm which is set when form is closed). I am using string, but it can be any data type obviously.
From the input of the answers here, I came to the following solution:
(Mis)Using the ReportProgress-method of the Backgroundworker in Combination with a EventWaitHandle. If I want to interact with the user, I call the ReportProgress-method and setting the background process on wait. In the Handler for the ReportProgress event I do the interaction and when finished, I release the EventWaitHandle.
BackgroundWorker bgw;
public MainWindow()
{
InitializeComponent();
bgw = new BackgroundWorker();
bgw.DoWork += new DoWorkEventHandler(bgw_DoWork);
bgw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bgw_RunWorkerCompleted);
bgw.WorkerReportsProgress = true;
bgw.ProgressChanged += new ProgressChangedEventHandler(bgw_ProgressChanged);
}
// Starting the time consuming operation
private void Button_Click(object sender, RoutedEventArgs e)
{
bgw.RunWorkerAsync();
}
// using the ProgressChanged-Handler to execute the user interaction
void bgw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
UserStateData usd = e.UserState as UserStateData;
// UserStateData.Message is used to see **who** called the method
if (usd.Message == "X")
{
// do the user interaction here
UserInteraction wnd = new UserInteraction();
wnd.ShowDialog();
// A global variable to carry the information and the EventWaitHandle
Controller.instance.TWS.Message = wnd.TextBox_Message.Text;
Controller.instance.TWS.Background.Set();
}
}
void bgw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
MessageBox.Show(e.Result.ToString());
}
// our time consuming operation
void bgw_DoWork(object sender, DoWorkEventArgs e)
{
Thread.Sleep(2000);
// need 4 userinteraction: raise the ReportProgress event and Wait
bgw.ReportProgress(0, new UserStateData() { Message = "X", Data = "Test" });
Controller.instance.TWS.Background.WaitOne();
// The WaitHandle was released, the needed information should be written to global variable
string first = Controller.instance.TWS.Message.ToString();
// ... and again
Thread.Sleep(2000);
bgw.ReportProgress(0, new UserStateData() { Message = "X", Data = "Test" });
Controller.instance.TWS.Background.WaitOne();
e.Result = first + Controller.instance.TWS.Message;
}
I hope I did not overlooked some critical issues. I'm not so familar with multithreading - maybe there should be some lock(object) somewhere?
I'm trying to write a program that could monitor multiple folders for file creations and launch the same action but with different settings for each folder. My problem is in specifying an extra parameter for the FileSystemEventHandler. I create a new FileWatcher for each directory to monitor and add the handler for the Created-action:
foreach (String config in configs)
{
...
FileWatcher.Created += new System.IO.FileSystemEventHandler(FileSystemWatcherCreated)
...
}
void FileSystemWatcherCreated(object sender, System.IO.FileSystemEventArgs e, MySettings mSettings)
{
DoSomething(e.FullPath, mSettings);
}
How could I get the 'mSettings' variable passed to FileSystemWatcherCreated()?
foreach (String config in configs)
{
...
MySettings mSettings = new MySettings(...); // create a new instance, don't modify an existing one
var handler = new System.IO.FileSystemEventHandler( (s,e) => FileSystemWatcherCreated(s,e,msettings) );
FileWatcher.Created += handler;
// store handler somewhere, so you can later unsubscribe
...
}
void FileSystemWatcherCreated(object sender, System.IO.FileSystemEventArgs e, MySettings mSettings)
{
DoSomething(e.FullPath, mSettings);
}
foreach (String config in configs)
{
...
FileWatcher.Created += (s,e) => DoSomething(e.FullPath, mSettings);
...
}
You can't ask for more information than what the FileWatcher handler provides. What you can do however is to create a small classes that have access to the configuration and also have a delegate that you can attach to the FileWatcher's Created event
class Program
{
static void Main(string[] args)
{
FileSystemWatcher watcher = new FileSystemWatcher("yourpath");
var configurations = new IConfiguration[]
{
new IntConfiguration(20),
new StringConfiguration("Something to print")
};
foreach(var config in configurations)
watcher.Created += config.HandleCreation;
}
private interface IConfiguration
{
void HandleCreation(object sender, FileSystemEventArgs e);
}
private class IntConfiguration : IConfiguration
{
public IntConfiguration(int aSetting)
{
ASetting = aSetting;
}
private int ASetting { get; set; }
public void HandleCreation(object sender, FileSystemEventArgs e)
{
Console.WriteLine("Consume your settings: {0}", ASetting);
}
}
public class StringConfiguration : IConfiguration
{
public string AnotherSetting { get; set;}
public StringConfiguration(string anotherSetting)
{
AnotherSetting = anotherSetting;
}
public void HandleCreation(object sender, FileSystemEventArgs e)
{
Console.WriteLine("Consume your string setting: {0}", AnotherSetting);
}
}
}
You need to understand what you are using. FileSystemEventHandler's definition is-
public delegate void FileSystemEventHandler(object sender, FileSystemEventArgs e);
You can't pass the third argument.
In order to pass the data 'mSettings', you might have to write your own extra code, I'm afraid.