问题
Currently, I have a button that when the user clicks on it, it looks for a specific CD-ROM drive that is ready and contains a file. Sometimes, when the user clicks on a button, the button click is mouse down and the program hangs for an indeterminate time until the computer reads the CD-ROM drive.
I made my progress bar but I noticed a few things:
1) The program hangs/freezes before the method that checks the cd drives gets called. So I can't set the progress bar to display when the method is called. Seems like the program hangs when the button is clicked on and while the user puts in the CD at the same time. How can I display the progress bar as soon as the button is clicked and the mouse is still down/until the system detects the cd drive?
2) I'm confused on how to implement Background Worker. I looked liked examples but none of them matched having an indeterminate progress bar with MVVM (No code behind) approach.
3) How do I make the window disappear after the operation is done? Currently, I have a cancel button (Definitely not useful).
Here's what I have set up so far. Not sure how to continue:
Progress Bar:
<Grid>
<Border BorderBrush="Black" BorderThickness="2" CornerRadius="4" Background="#EEEEEE" HorizontalAlignment="Left" Height="110" VerticalAlignment="Top" Width="295" />
<StackPanel>
<Label x:Name="lblProgress"/>
<ProgressBar x:Name="progress" Height="25" Width="270" IsIndeterminate="True" Foreground="Green"></ProgressBar>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="225,10,0,0" RenderTransformOrigin="0.083,0.526">
<Button x:Name="btnCancel" Width="60" Content="Cancel" Command="{Binding CloseCommand}"/>
</StackPanel>
</StackPanel>
</Grid>
I have a ProgressBarViewModel that contains the command that allows the user to cancel the progress window.Also, I have another ViewModel that I need to call the progressBar Dialog inside but I'm not sure where to call it because if I call it inside my method, the button still hangs without displaying a progressbar.
I noticed if I use Button_PreviewMouseDown method in codebehind, however, the progress bar appears correctly when the mouse is down and the system does display the progress bar but I don't want to use codebehind because I have the progress bar in another view.
Currently, for my import button, all that is attached is a command that calls a method that searches drives for a CD-ROM drive.
MainViewModel:
public ICommand ImportCDFilePathCommand
{
get
{
return new RelayCommand(ImportCDFilePath, null);
}
}
private void ImportCDFilePath()
{
// dialogService.ShowDialog("Progress", progressBarWindow); <---Does not get called until the operation is done
//Gets all the drives
DriveInfo[] allDrives = DriveInfo.GetDrives();
//checks if any CD-Rom exists in the drives
var cdRomExists = allDrives.Any(x => x.DriveType == DriveType.CDRom);
// Get all the cd roms
var cdRoms = allDrives.Where(x=>x.DriveType==DriveType.CDRom && allDrives.Any(y=>y.IsReady));
//.... There is other code that is commented out too long and not necessary
}
EDIT:
Some attempts using BackgroundWorker:
static BackgroundWorker _bw = new BackgroundWorker();
//constructor
MainViewModel() {
_bw.DoWork += bw_DoWork;
_bw.RunWorkerAsync("Message to worker");
}
void bw_DoWork(object sender, DoWorkEventArgs e)
{
// This is called on the worker thread
Console.WriteLine(e.Argument); // writes "Message to worker"
// Perform time-consuming task...
ImportCDFilePath();
}
Error I get:
The calling thread must be STA, because many UI components require this.
回答1:
Hi I was a bit fast here, the methods you were using did not have any async-await overloads. So you could use the old BackgroundWorker. I've provided a really simple example for you here, made quickly (making food). The (unrun) example will only report progress 0 or 100, but it will not freeze your UI atleast. When reporting progress you send an int (the progress) and a userstate object, which may be whatever you want to send. Just cast it and do what you want :)
public class TestViewModel : INotifyPropertyChanged
{
private int progress;
private BackgroundWorker bgWorker;
private bool isBusy;
private readonly Dispatcher dispatcher;
private ObservableCollection<DriveInfo> cdRoms;
public Int32 Progress
{
get { return progress; }
set
{
if (value == progress) return;
progress = value;
OnPropertyChanged();
}
}
public bool IsBusy
{
get { return isBusy; }
set
{
if (value.Equals(isBusy)) return;
isBusy = value;
OnPropertyChanged();
}
}
public ICommand ImportCDFilePathCommand
{
get
{
return new RelayCommand(ImportReagentLotFilePath);
}
}
public ObservableCollection<DriveInfo> CdRoms
{
get { return cdRoms; }
set
{
if (Equals(value, cdRoms)) return;
cdRoms = value;
OnPropertyChanged();
}
}
// This one made your app crash if you defined it directly in the xaml as datacontext and not were using a viewmodellocator
public TestViewModel(Dispatcher dispatcher) // ugh I'm sure there is an interface for this, feed your UI dispatcher here
{
this.dispatcher = dispatcher;
}
// Add this one!
public TestViewModel()
{
this.dispatcher = App.Current.Dispatcher; // Bad pie
}
private void ImportReagentLotFilePath()
{
IsBusy = true;
Progress = 0;
bgWorker = new BackgroundWorker { WorkerReportsProgress = true, WorkerSupportsCancellation = true };
bgWorker.DoWork += bgWorker_DoWork;
bgWorker.ProgressChanged += bgWorker_ProgressChanged;
bgWorker.RunWorkerCompleted += bgWorker_RunWorkerCompleted;
bgWorker.RunWorkerAsync(/*whatever parameter you want goes here*/);
}
void bgWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
// you are done!
Progress = 100;
CdRoms = new ObservableCollection<DriveInfo>(e.UserState as IEnumerable<DriveInfo>);
}
void bgWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
// Notifty your gui changes here forinstance, this method will be called on the gui thread. Just cast/parse what you feed
Progress = e.ProgressPercentage;
if (Progress == 100)
IsBusy = false;
}
void bgWorker_DoWork(object sender, DoWorkEventArgs e)
{
try
{
DriveInfo[] allDrives = DriveInfo.GetDrives();
bool cdRomExists = allDrives.Any(x => x.DriveType == DriveType.CDRom);
IEnumerable<DriveInfo> cdroms = allDrives.Where(x => x.DriveType == DriveType.CDRom && allDrives.Any(y => y.IsReady));
// reports the progress on the ui thread....
bgWorker.ReportProgress(Progress,cdroms);
}
catch (Exception ex)
{
// errror handling + cancel run
dispatcher.BeginInvoke((Action) (() => { IsBusy = false; Progress = 0; }));
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator] // remove if you are not using R#
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Using tasks:
// Alternatively use a task.....
public ICommand TaskTestCommand
{
get
{
return new RelayCommand(DoStuffAsync);
}
}
public Task DoStuffAsync()
{
Task tcs = Task.Factory.StartNew(() =>
{
try
{
// No awaits... please note that anything bound in the gui must be changed on the dispatcher
DriveInfo[] allDrives = DriveInfo.GetDrives();
bool cdRomExists = allDrives.Any(x => x.DriveType == DriveType.CDRom);
IEnumerable<DriveInfo> cdroms = allDrives.Where(x => x.DriveType == DriveType.CDRom && allDrives.Any(y => y.IsReady));
}
catch (Exception ex)
{
// handle your errors here. Note that you must check the innerexception for the real fault
System.Diagnostics.Trace.WriteLine(ex.ToString());
}
}).ContinueWith((e) => { // this code is run when the task is completed...
if(e.Exception!=null)
{
// hande error.. /
}
else
{
// complete.. do whatever here
}
});
return tcs;
}
Hope it helps you in the right direction! I'm actually a bit surprised that there were no async-await overloads of the methods you are using, because it would allow you to use the nice async-await "statemachine-auto treader".
Cheers,
Stian
回答2:
You can wrap any method in async using the following...
await Task.Factory.StartNew<T>(()=>{ ... Do something here...});
T is a generic to return a type. In your relay command example:
... = new RelayCommand(async () =>{ await DoSomethingAsync()});
Then...
private async DoSomethingAsync()
{
await Task.Factory.StartNew(()=> {...});
}
Where your ... is whatever you want.
回答3:
With the help of Julien and NetSCAPE on SO WPF Chat, I figured out a few things I was doing wrong:
-I had dialog messages inside my background thread that should not be there in the method that was executing background threads. Thus, that's why I kept getting the STA error.
-All I needed to do was use Task.Run or Task.Factory.StartNew instead of pairing that with await/async because it was waiting for a task to be executed before running.
Here's how my answer looks now with removed dialog messages:
private void DoSomethingAsync()
{
ProgressBarVisibility = Visibility.Visible;
Task.Factory.StartNew(() => { PerformCDDetection(); }).ContinueWith(t => { ProgressBarVisibility = Visibility.Collapsed; });
}
public ICommand ImportFilePathCommand
{
get
{
return new RelayCommand(() => { DoSomethingAsync(); });
}
}
private void PerformCDDetection()
{
//Gets all the drives
DriveInfo[] allDrives = DriveInfo.GetDrives();
//checks if any CD-Rom exists in the drives
var cdRomExists = allDrives.Any(x => x.DriveType == DriveType.CDRom);
// Get all the cd roms
var cdRoms = allDrives.Where(x => x.DriveType == DriveType.CDRom && allDrives.Any(y => y.IsReady));
if (cdRomExists.Equals(true))
{
// Loop through the cd roms collection
foreach(var cdRom in cdRoms)
{
Console.WriteLine("Drive {0}", cdRom.Name);
Console.WriteLine(" File type: {0}", cdRom.DriveType);
if (cdRom.IsReady == true)
{
if (cdRom.DriveType == DriveType.CDRom)
{
DirectoryInfo di = new DirectoryInfo(cdRom.RootDirectory.Name);
var file = di.GetFiles("*.xml", SearchOption.AllDirectories).FirstOrDefault();
if (file == null)
{
Console.WriteLine("failed to find file");
}
else
{
foreach (FileInfo info in di.GetFiles("*.xml", SearchOption.AllDirectories))
{
Debug.Print(info.FullName);
break; // only looking for the first one
}
break;
}
}
else if (cdRom.IsReady == false)
{
Console.WriteLine("Cd-ROM is not ready");
break;
}
}
}
else
{
Console.WriteLine("CD ROM is not detected");
}
}
}
来源:https://stackoverflow.com/questions/25021838/indeterminate-progress-bar