Emulating blocking Console keyboard input in a form - c#

I need to write a console emulator in C#. I want to replace System.Console with something that can work in full-screen. My approach is to make a maximized Form with no borders.
One challenge I am facing is how to convert the event-driven, non-blocking keyboard input into a blocking one, or more specifically, how would you go about implementing an equivalent of Console.ReadKey() in a Windows Form.

I don't think that you really want to have fully blocking console window. What you need is more like PowerShell or standard console window. I mean that it is doubtful that you will be against having the ability to move the window, copy data from the console and so on.
So, the console described further is based upon TextBox. Why not on the Form itself? - Because multiline, readonly TextBox already provide a lot of facilities that will simplify our Console (AppendText, text selection, copying ...).
1. Idea and interfaces:
As you have already mentioned in the question, WinForms have completely different model from console application with message queue and event based processing. So, to get data from event and prevent form from becoming irresponsive we cannot block this event loop. To achieve such behaviour we will run our console-based code on different thread with Task.Run and unblock input-dependent "console" calls from events in main form thread. The cornerstones of the application will be the next two interfaces:
public interface IConsole
{
ConsoleKeyInfo ReadKey();
void WriteLine(String line);
}
public interface IConsoleContext
{
void Run(Action<IConsole> main);
}
First is the Console itself with methods that reflect standard console capabilities. Second will run our console related code(represented by action) on some another thread, providing some console object to this action.
Our IConsoleContext implementation will be TextBoxConsoleContext with its private nested class TextBoxConsole as IConsole
2. Main form code
Here is the code we will use in our form to demonstrate the console. Assuming that we have textBox textBox_Console:
private void Form1_Load(object sender, EventArgs e)
{
var consoleContext = new TextBoxConsoleContext(this.textBox_Console);
consoleContext.Run((console) =>
{
console.WriteLine("Welcome to the TextBox console");
console.WriteLine("Press any key:");
console.ReadKey();
ConsoleKeyInfo keyInfo = new ConsoleKeyInfo();
console.WriteLine("Press y to continue: ");
do
{
keyInfo = console.ReadKey();
if (keyInfo.KeyChar == 'y')
break;
console.WriteLine("You have entered another key, please enter y to continue:");
}
while (true);
console.WriteLine("Thank you for your cooperation.");
});
}
3. The TextBoxConsoleContext itself:
public class TextBoxConsoleContext : IConsoleContext
{
#region Nested types
private class TextBoxConsole : IConsole
{
#region Fields
private TextBoxConsoleContext parent;
#endregion
#region Constructors
public TextBoxConsole(TextBoxConsoleContext parent)
{
if (parent == null)
throw new ArgumentNullException("parent");
this.parent = parent;
}
#endregion
#region IConsole implementation
public ConsoleKeyInfo ReadKey()
{
var key = this.parent.m_Queue.Dequeue();
this.WriteLine(key.KeyChar.ToString());
return key;
}
public void WriteLine(string line)
{
Action writeLine = () =>
{
var textToAppend = String.Format("{0}{1}",
line,
Environment.NewLine);
this.parent.m_TextBox.AppendText(textToAppend);
};
this.parent.m_TextBox.Invoke(writeLine);
}
#endregion
}
#endregion
#region Fields
private TextBox m_TextBox;
private OnRequestProducerConsumerQueue<ConsoleKeyInfo> m_Queue = new OnRequestProducerConsumerQueue<ConsoleKeyInfo>();
private Boolean m_Shift;
private Boolean m_Alt;
private Boolean m_Ctrl;
private ConsoleKey m_KeyInfo;
#endregion
#region Constructors
public TextBoxConsoleContext(TextBox textBox)
{
if (textBox == null)
throw new ArgumentNullException("textBox");
this.m_TextBox = textBox;
this.m_TextBox.ReadOnly = true;
// Event handler that will read key down data before key press
this.m_TextBox.KeyDown += (obj, e) =>
{
this.m_Shift = e.Modifiers.HasFlag(Keys.Shift);
this.m_Alt = e.Modifiers.HasFlag(Keys.Alt);
this.m_Ctrl = e.Modifiers.HasFlag(Keys.Control);
if (!Enum.TryParse<ConsoleKey>(e.KeyCode.ToString(), out this.m_KeyInfo))
{
this.m_KeyInfo = ConsoleKey.Escape;
}
};
this.m_TextBox.KeyPress += (obj, e) =>
{
this.m_Queue.EnqueueIfRequired(new ConsoleKeyInfo(e.KeyChar,
this.m_KeyInfo,
this.m_Shift,
this.m_Alt,
this.m_Ctrl));
};
}
#endregion
#region IConsoleContext implementation
public void Run(Action<IConsole> main)
{
if (main == null)
throw new ArgumentNullException("main");
var console = new TextBoxConsole(this);
Task.Run(() =>
main(console));
}
#endregion
}
4. OnRequestProducerConsumerQueue
EDIT: I have replaced initial queue class with this much simpler class that uses single lock object with Monitor calls. This code is based on the condition variable pattern.
REMARK: I just wanted to replace the previous abuse of sync constructs I have posted initially. This version doesn't change anything in the behaviour, and the EnqueueIfRequired in this implementation will be more than useless if you need to implement ReadLine methods (but you can add to it some predicate logic that will allow queening till the line terminator appears).
public class OnRequestProducerConsumerQueue<T>
{
private Queue<T> m_Items = new Queue<T>();
private object m_lock = new Object();
private Int32 m_NeedItems = 0;
public void EnqueueIfRequired(T value)
{
lock (this.m_lock)
{
if (this.m_NeedItems == 0)
return;
this.m_Items.Enqueue(value);
this.m_NeedItems--;
Monitor.PulseAll(this.m_lock);
}
}
public T Dequeue()
{
lock (this.m_lock)
{
this.m_NeedItems++;
while (this.m_Items.Count < 1)
{
Monitor.Wait(this.m_lock);
}
return this.m_Items.Dequeue();
}
}
}

Related

Sending Update Events to Multiple Forms from a different Class - WPF

The Structure
I have a simple form that fires off a timer that checks for updates pretty regularly. The constructor of the form that starts on load looks like so:
public MainWindow()
{
InitializeComponent();
otherWindow = new TheOtherWindow();
if (Meta.hasUpdate)
{
updateImage.Source = new BitmapImage(new Uri("/MyProject;component/Images/updateTrue.gif", UriKind.Relative));
}
Thread updateMonitor = new Thread(() =>
{
UpdateManager updater = new UpdateManager();
updater.StartUpdateMonitor();
});
updateMonitor.IsBackground = true;
updateMonitor.Start();
}
The Meta class contains some very basic information, storing various strings that are referenced in several places but are sometimes updated. Among that structure is this:
class Meta
{
...
private static bool hasUpdate = false;
public static bool GetHasUpdate()
{
return hasUpdate;
}
public static void SetHasUpdate(bool value)
{
hasUpdate = value;
}
}
The other piece is the UpdateManager class, which includes this a small routine to check for an update every 5 minutes.
class UpdateManager
{
Timer timer;
public void CheckForUpdates(Object source, ElapsedEventArgs e)
{
if (!isUpToDate())
{
timer.Stop();
Meta.SetHasUpdate(true);
Application.Current.Dispatcher.Invoke(new Action(() =>
{
MessageBox.Show("A new update is now available!);
}));
}
}
public void StartUpdateMonitor()
{
float updateInterval = 300000;
timer = new Timer(updateInterval); // Milliseconds between checks.
timer.Elapsed += CheckForUpdates;
timer.AutoReset = true;
timer.Enabled = true;
}
}
The Problem
In short, I want to fire off an event whenever Meta.SetHasUpdate() is reached that then broadcasts this to all the forms in the application with the goal of changing a small icon to indicate that an update is available.
My attempts to do so have ended with me learning that implementing INotifyPropertyChanged does not play nice with Static members. This was my attempt in implementing that...
class Meta : INotifyPropertyChanged
{
...
private static bool hasUpdate = true;
public static bool GetHasUpdate()
{
return hasUpdate;
}
public static void SetHasUpdate(bool value)
{
hasUpdate = value;
NotifyPropertyChanged();
}
private static void NotifyPropertyChanged()
{
if (PropertyChanged != null)
{
PropertyChanged(null, new PropertyChangedEventArgs("hasUpdate"));
}
}
}
Since these members need to be read back from multiple forms, I can't make them not static without passing an object around a lot, which I don't want to do.
How do fire off an event that multiple forms can receive from the Meta class in this case? Do I need to consider a different structure, or am I misunderstanding INotifyPropertyChanged?
While there can be many ways to solve this, (think DI of your Meta class into each of your pages' ViewModels and react to INPC..that would be preferred over singleton approach), one approach to consider is using Messaging rather than Events. Messages, (offered in most MVVM frameworks), are great way to communicate between loosely coupled components. If you leverage an MVVM Library like MVVM Light, then this is very easy as it includes a Messenger implementation. The main advantage of this approach is that the forms that you want to receive the notification don't necessarily need to hold on to a reference of the source, like you would with an Event based approach.
Simply have all interested forms register for a message, and react accordingly when received.
For example, with MVVM Light, we can take advantage of automatically broadcasting a message when a INPC property has been updated.
private bool hasUpdate;
public bool HasUpdate
{
{
return hasUpdate;
}
set
{
// the last bool param indicates whether or not to broadcast a message to all interested parties.
Set(nameof(HasUpdate), ref hasUpdate, value, true);
}
}
Then in a totally separate / unrelated part of the app, (usually in a ViewModel), we can do this to indicate that we are interested in such an update:
Messenger.Default.Register<PropertyChangedMessage<bool>>(this, m => ReceiveHasUpdatedMessage(m));
and then in the receiving lambda:
private void ReceiveHasUpdatedMessage(PropertyChangedMessage<bool> m)
{
// react accordingly.
}
This is just one simple use case of the Messenger that MVVM Light provides.. you can do pretty much anything you want. The premise here is that using this approach decouples interested parties from requiring a hard reference to the emitter.
With a combination of everyone's very helpful advice, I've put together the following code. The MVVM solution is above, although I did not test it. If you aren't using MVVM though, this is what I did.
The UpdateManager class is the same. Meta has the following structure:
class Meta
{
private static bool hasUpdate = false;
public static event PropertyChangedEventHandler StaticPropertyChanged;
public static bool GetHasUpdate()
{
return hasUpdate;
}
public static void SetHasUpdate(bool value)
{
hasUpdate = value;
StaticNotifyPropertyChanged();
}
private static void StaticNotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
StaticPropertyChanged?.Invoke(null, new PropertyChangedEventArgs(propertyName));
}
}
Then, for any form I want to be aware of this kind of a change, I bolt in the following code:
public partial class SomeForm : Window
{
public SomeForm()
{
InitializeComponent();
Meta.StaticPropertyChanged += MethodThatTriggersOnUpdate;
...
}
private void MethodThatTriggersOnUpdate(object sender, EventArgs e)
{
myImage.Dispatcher.BeginInvoke(
(Action)(() => myImage.Source = new BitmapImage(
new Uri("/MyProject;component/Images/myNewImage.gif", UriKind.Relative))));
}
...
}

User interaction in non-UI thread?

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?

C# Cross threading. IRC stream thread to Main UI thread

I've been trying to get this little IRC program working but for some reason I'm having issues with VS and cross threading. I'm not sure if I'm not doing it the proper way or what. Here are the parts causing the issue.
Main Thread:
public partial class MainUI : Form
{
private static IRC irc = new IRC();
public MainUI()
{
InitializeComponent();
}
public static void StartIRC()
{
irc.Start();
}
}
IRC Thread:
class IRC
{
private Thread ircThread;
private bool _running = true;
private NetworkStream stream;
private StreamWriter writer;
private StreamReader reader;
private TcpClient irc;
public IRC(){
ircThread = new Thread(new ThreadStart(Run));
ircThread.IsBackground = true;
}
public void Run(){
while (_running) {
parseInStream(reader.ReadLine());
}
}
public void Start()
{
ircThread.Start();
}
private void parseInStream(String inText)
{
String[] text = inText.Split(' ');
String name;
String message;
if (text[1].Equals("PRIVMSG")) {
name = capsFirstChar(getUser(inText));
message = inText.Substring(inText.IndexOf(":", 1) + 1);
sendToChatBox(capsFirstChar(name) + ": " + message, Color.Black);
}
else if (text[1].Equals("JOIN")) {
name = getUser(inText);
sendToChatBox(capsFirstChar(name) + " has joined the channel.", Color.LimeGreen);
}
else if (text[1].Equals("PART")) {
name = getUser(inText);
sendToChatBox(capsFirstChar(name) + " has left the channel.", Color.Red);
}
}
public void sendToChatBox(String text, Color color)
{
//Trying to send the text to the chatbox on the MainUI
//Works if the MainUI.Designer.cs file has it set to static
if (MainUI.txtMainChat.InvokeRequired) {
MainUI.txtMainChat.Invoke((MethodInvoker)delegate() {
sendToChatBox(text, color);
});
}
else {
MainUI.txtMainChat.SelectionColor = color;
MainUI.txtMainChat.AppendText(text);
}
}
private String getUser(String msg)
{
String[] split = msg.Split('!');
user = split[0].Substring(1);
return capsFirstChar(user);
}
private String capsFirstChar(String text)
{
return char.ToUpper(text[0]) + text.Substring(1).ToLower();
}
}
The only way I am able to get it to work is if I enter the MainUI.Designer.cs file and change the textbox to static and then change everything from this.txtMainChatto MainUI.txtMainChat.
My main problem is that when I make any changes on the visual side all the things labeled static or things named MainUI are deleted. I'm trying to figure out what I need to do to keep this from happening. Am I doing it the right way, or is there a better way? I tried using a background worker but it was using a lot of processing power to work that way for some reason.
I've looked around the web and can't seem to find out how one might relate to my setup. I see people calling a thread from the main thread and then sending things from the main thread to the thread it called but not the other way around. There is nothing else being written to the text box so there won't be an issue with it being used by two threads at the same time.
On my main UI thread I passed in "this" so I could reference the main window from my IRC Class. MainUI.txtMainChat
irc = new IRC(this);
Then in my IRC class
MainUI main;
public IRC(MainUI main){
this.main = main;
ircThread = new Thread(new ThreadStart(Run));
ircThread.IsBackground = true;
}
Then I was able to Change
//MainUI.txtMainChat to
main.txtMainChat
Like Cameron said, Though I know I was told it's not the best approach it gets me started.
Your designer file is rebuilt every time you change your UI in the designer.
You'll need to pass your MainUi to your IRC class, or give it an abstraction of it using an interface (best option).
public interface IMainUI
{
void AddText(string text, Color color);
void UiThread(Action code);
}
public class MainUI : IMainUI
{
// Whatever else
public void AddText(string text, Color color)
{
UiThread( () =>
{
// Same code that was in your Irc.SendToChatBox method.
});
}
public void UiThread(Action code)
{
if (InvokeRequired)
{
BeginInvoke(code);
return;
}
code.Invoke();
}
}
public class IRC
{
IMainUI _mainUi;
//Other properties, and fields
public IRC(IMainUI mainUi)
{
this._mainUi = mainUi;
// Other constructor stuff.
}
// Other logic and methods
}

Doesn't work using thread but works if code is inside win form [duplicate]

This question already has answers here:
How do I update the GUI from another thread?
(47 answers)
Closed 9 years ago.
I have looked for a solution for my problem, but I couldn't find it yet.
I'm coding a bot which will be connected to an irc server. I want the user to have access to a win forms window where it is possible to input the server, channel, and so on.
This is the window:
I have set the console output to update my textbox with the code in the main program. With this, I get all text coming from the child classes into my text box.
static class Program
{
private static Form1 Form1;
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
using (var consoleWriter = new ConsoleWriter())
{
consoleWriter.WriteLineEvent += consoleWriter_WriteLineEvent;
Console.SetOut(consoleWriter);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Form1 = new Form1();
Application.Run(Form1);
}
}
static void consoleWriter_WriteLineEvent(object sender, ConsoleWriterEventArgs msg)
{
var message = msg.Value;
Form1.statusTextBox.AppendText(message + "\r\n");
}
}
public class ConsoleWriter : TextWriter
{
public override Encoding Encoding { get { return Encoding.UTF8; } }
public override void Write(string value)
{
if (WriteEvent != null) WriteEvent(this, new ConsoleWriterEventArgs(value));
base.Write(value);
}
public override void WriteLine(string value)
{
if (WriteLineEvent != null) WriteLineEvent(this, new ConsoleWriterEventArgs(value));
base.WriteLine(value);
}
public event EventHandler<ConsoleWriterEventArgs> WriteEvent;
public event EventHandler<ConsoleWriterEventArgs> WriteLineEvent;
}
public class ConsoleWriterEventArgs : EventArgs
{
public string Value { get; private set; }
public ConsoleWriterEventArgs(string value)
{
Value = value;
}
}
Now I call this in my Form1, when the button "Connect" is clicked on:
gbaIrcBot.Connect(server, null, port, nick, channelList);
Inside the gbaIrcBot.Connect(), i have among other things:
private void ReadStream()
{
string inputLine;
while ((inputLine = _streamReader.ReadLine()) != null)
{
var splitInput = inputLine.Split(new[] { ' ' });
if (splitInput.ElementAt(0) == "PING") { Pong(splitInput.ElementAt(1)); continue;}
switch (splitInput.ElementAt(1))
{
case "001": foreach (var channel in ChannelList) JoinChannel(channel); break;
case "PRIVMSG": ProcessPrivMsg(splitInput); break;
case "433": SetNick("AlternativeBOT"); break;
default: Console.WriteLine(inputLine); break;
}
}
}
This method is responsible for reading all inputs from the irc server. When I get messages from the server, I send it to the console, which updates the textbox in Form1. It MUST be an infinite loop.
All this works well if I dont create a thread to keep my UI not frozen. This is an example:
When I try to create a thread, My Form1 throws an exception saying its a cross-thread message, and I cannot update it from outside.
Any idea to solve it?
WinForms UI updates have to be performed on the UI thread. Try this approach:
How to update the GUI from another thread in C#?
The update on the form must be done in the window update thread.
You can force the execution in such thread by wrapping your call with BeginInvoke().
Change
Form1.statusTextBox.AppendText(message + "\r\n");
to
Form1.BeginInvoke(() => Form1.statusTextBox.AppendText(message + "\r\n"));

WPF - Updating Label Content During Processing

Well I've tried several methods of getting this to work, background worker, Dispatcher.Invoke, threading within the called class and nothing seems, to work. The best solution so far is an Extension method which calls the invoke of the control. Also I've tried avoid passing the data for the label through my event classes and simply invoking within my processing code, however this made no difference.
In regards to the background component, I kept getting exceptions saying the background worker was busy, so I instantiated the class several times, however the label only visibly changed once the entire operation had been complete.
I've removed my previous code, here's everything that is relevant, as it seems the issue is difficult to resolve.
Method Being Called
private void TestUris()
{
string text = new TextRange(rtxturis.Document.ContentStart, rtxturis.Document.ContentEnd).Text;
string[] lines = Regex.Split(text.Remove(text.Length - 2), "\r\n");
foreach (string uri in lines)
{
SafeUpdateStatusText(uri);
bool result;
string modUri;
if (!uri.Contains("http://"))
{
modUri = uri;
result = StoreData.LinkUriExists(new Uri("http://" + modUri));
}
else
{
modUri = uri.Substring(7);
result = StoreData.LinkUriExists(new Uri(uri));
}
if (!result)
{
Yahoo yahoo = new Yahoo();
yahoo.Status.Sending += (StatusChange);
uint yahooResult = 0;
yahooResult = yahoo.ReturnLinkCount(modUri);
if (yahooResult > 1000 )
{ results.Add(new ScrapeDetails(Guid.NewGuid(), modUri, 1000, "Will be processed", true)); }
else
{ results.Add(new ScrapeDetails(Guid.NewGuid(), modUri, (int)yahooResult, "Insufficient backlinks", false)); }
}
else
{
results.Add(new ScrapeDetails(Guid.NewGuid(), modUri, 0, "Previously been processed", false));
}
}
foreach (var record in results)
{
dgvresults.Items.Add(record);
}
EnableStartButton();
}
Yahoo Class
public class Yahoo
{
/// <summary>
/// Returns the amount of links each Uri has.
/// </summary>
public uint ReturnLinkCount(string uri)
{
string html;
Status.Update(uri, false); //this is where the status is called
try
{
html = client.DownloadString(string.Format("http://siteexplorer.search.yahoo.com/search?p=http%3A%2F%2F{0}&fr=sfp&bwm=i", uri));
}
catch (WebException ex)
{
ProcessError(ex.ToString());
return 0;
}
return (LinkNumber(html));
}
Status Classes
public class StatusEventArgs : EventArgs
{
private string _message;
private bool _isidle;
public StatusEventArgs(string message, bool isidle)
{
this._message = message;
this._isidle = isidle;
}
public bool IsIdle
{
get { return _isidle; }
}
public string Message
{
get { return _message; }
}
}
public class Status
{
public Status()
{
}
// Declaring an event, with a custom event arguments class
public event EventHandler<StatusEventArgs> Sending;
// Some method to fire the event.
public void Update(string message, bool isIdle)
{
StatusEventArgs msg = new StatusEventArgs(message, isIdle);
OnUpdate(msg);
}
// The method that invokes the event.
protected virtual void OnUpdate(StatusEventArgs e)
{
EventHandler<StatusEventArgs> handler = Sending;
if (handler != null)
{
handler(this, e);
}
}
}
Method That Changes the labels Content
private void StatusChange(object sender, StatusEventArgs e)
{
if(!e.IsIdle)
{
lblstatus.Content = e.Message;
lblstatus.Foreground = StatusColors.Green;
lblstatus.Refresh();
}
else
{
lblstatus.Content = e.Message;
lblstatus.Foreground = StatusColors.Grey;
lblstatus.Refresh();
}
}
The Refresh static method called:
public static class ExtensionMethods
{
private static Action EmptyDelegate = delegate() { };
public static void Refresh(this UIElement uiElement)
{
uiElement.Dispatcher.Invoke(DispatcherPriority.Render , EmptyDelegate);
}
Another EDIT: Staring at my code for a bit longer, I've realised, that the foreach loop will execute really quickly, the operation which takes the time, is
yahooResult = yahoo.ReturnLinkCount(modUri);
Therefore I've declared the status class (which handles the event and invokes the label etc) and subscibed to it. I've gotten better results, although it still feels random, sometimes I see a couple of label updates, and sometimes one even though the exact same URI's are passed, so weird.
I hope there is sth. helpful...
private void button1_Click(object sender, RoutedEventArgs e)
{
ThreadPool.QueueUserWorkItem(o =>
{
int result = 0;
for (int i = 0; i < 9999999; i++)
{
result++;
Dispatcher.BeginInvoke(new Action(() =>
{
this.label1.Content = result;
}));
Thread.Sleep(1);
}
});
}
SOLVED IT YES WOOHOOOOOOOO 3 days of testing, testing, testing.
I decided to start a new project just with the extension method above and a simply for loop to test UI update functionality. I started testing different DispatchPrioraties (tested them all).
Weirdly, I found the highest priorities were the worse, for example using Send didn't update the label at all, Render updated it twice on average. This was the weird behavior I was experiencing as I tried different priorities. I discovered Background:
The enumeration value is 4. Operations are processed after all other non-idle operations are completed.
Now this sounded exactly what I didn't want, as obviously the label should update during processing, hence why I never tried it. I'm guessing that once one of my method has been completed, before the next it called, the UI is updated. I'm find of guessing, but it 100% consistently updates correctly on two separate operations.
Thanks all.
Well this is going to sound stupid but you could just reference the forms namespace, and then you can do this
using System.Windows.Forms;
mylabel = "Start";
Application.doEvents();
myLabel = "update"
Application.doEvents();
now the problem using this would be you are using wpf but you can still reference forms and use this namespace. The other issue is what ever is in the que would execute directly to the ui. However this is the most simplistic way of doing label updates i could think of.
would it be easier/better to add the status info as a property on this object, and have it just fire property change notifications?
that way the label text (or whatever) could be bound to the property instead of having the async work try to update a label?
or add a method like this to update status if you have to update it?
void SafeUpdateStatusText(string text)
{
// update status text on event thread if necessary
Dispatcher.BeginInvoke(DispatcherPriority.Background, (SendOrPostCallback)delegate
{
lblstatus.Content = text;
}, null);
}
otherwise, i don't think we have enough details to help yet....
I hope this helps:
private delegate void UpdateLabelDelegate(DependencyProperty dp, object value);
public void UpdateLabelContent(Label label, string newContent)
{
Dispatcher.Invoke(new UpdateLabelDelegate(label.SetValue), DispatcherPriority.Background, ContentProperty, newContent);
}
Usage:
while (true)
{
UpdateLabelContent(this.lblStatus, "Next random number: " + new Random().Next());
Thread.Sleep(1000);
}

Categories

Resources