Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
601 views
in Technique[技术] by (71.8m points)

c# - Indeterminate Progress Bar

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.
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...