This question already has answers here:
Is it possible to await an event instead of another async method?
(11 answers)
Closed 8 months ago.
How to Wait for a RoutedEvent? Since there is no definition for GetAwaiter i can't use the await keyword. So how should i modify the Code so that it waits for completion of the GIF Animation?
The Code who calls the RoutedEvent:
public static async Task<Boolean> Draw(List<Point> points, int counter, Polyline current, Color color, MainWindow main)
{
current.Stroke = new SolidColorBrush(color);
current.Points.Clear();
for (int i = 0; i < points.Count; i++)
{
await Task.Delay(TimeSpan.FromMilliseconds(1));
current.Points.Add(points[i]);
if(i == points.Count - 1)
{
var completed = new XamlAnimatedGif.AnimationCompletedEventArgs(current);
await ShowExplosion(main, current.Points.ElementAt(i));
}
}
points.Clear();
current = null;
return true;
}
an the RoutedEvent:
static RoutedEvent ShowExplosion(MainWindow main, Point coordinate)
{
Image explosion = new Image();
explosion.Width = 40;
explosion.Height = 40;
Canvas.SetLeft(explosion, coordinate.X - 20);
Canvas.SetTop(explosion, coordinate.Y - 20);
//Relative URI
var resourcePath = new Uri("pack://application:,,,/Resources/GIF/Explosion2_trsp.gif");
XamlAnimatedGif.AnimationBehavior.SetSourceUri(explosion, resourcePath);
XamlAnimatedGif.AnimationBehavior.SetRepeatBehavior(explosion, new System.Windows.Media.Animation.RepeatBehavior(1));
XamlAnimatedGif.AnimationBehavior.SetAutoStart(explosion, true);
main.canvas_shots.Children.Add(explosion);
return XamlAnimatedGif.AnimationBehavior.AnimationCompletedEvent;
}
You don't await events. Events are synchronous and a different concept than asynchronous methods. While you await asynchronous methods, you listen to events by registering an event handler i.e. callback with the event source (Events (C# Programming Guide), Routed events overview (WPF .NET)).
Unless you have a reference to the event source, you attach an event handler to an element along the event route using the UIElement.AddHandler method. In your case the parent MainWindow:
main.AddHandler(XamlAnimatedGif.AnimationBehavior.AnimationCompletedEvent, OnAnimationCompleted);
Your fixed code:
public static bool Draw(List<Point> points, int counter, Polyline current, Color color, MainWindow main)
{
current.Stroke = new SolidColorBrush(color);
current.Points.Clear();
for (int i = 0; i < points.Count; i++)
{
current.Points.Add(points[i]);
if (i == points.Count - 1)
{
var completed = new XamlAnimatedGif.AnimationCompletedEventArgs(current);
ShowExplosion(main, current.Points.ElementAt(i));
}
}
points.Clear();
current = null;
return true;
}
static void ShowExplosion(MainWindow main, Point coordinate)
{
Image explosion = new Image();
explosion.Width = 40;
explosion.Height = 40;
Canvas.SetLeft(explosion, coordinate.X - 20);
Canvas.SetTop(explosion, coordinate.Y - 20);
//Relative URI
var resourcePath = new Uri("pack://application:,,,/Resources/GIF/Explosion2_trsp.gif");
XamlAnimatedGif.AnimationBehavior.SetSourceUri(explosion, resourcePath);
XamlAnimatedGif.AnimationBehavior.SetRepeatBehavior(explosion, new System.Windows.Media.Animation.RepeatBehavior(1));
XamlAnimatedGif.AnimationBehavior.SetAutoStart(explosion, true);
main.canvas_shots.Children.Add(explosion);
// Listen to the event by registering a callback (event handler)
main.AddHandler(XamlAnimatedGif.AnimationBehavior.AnimationCompletedEvent, OnAnimationCompleted);
}
private void OnAnimationCompleted(object sender, AnimationCompletedEventArgs e)
{
var main = sender as UIElement;
main.RemoveHandler(XamlAnimatedGif.AnimationBehavior.AnimationCompletedEvent, OnAnimationCompleted);
// TODO::Do something after the animation has completed...
}
Related
This question already has answers here:
Error: Must create DependencySource on same Thread as the DependencyObject even by using Dispatcher
(2 answers)
Closed 3 years ago.
I am using a label on multiple forms which display the weather data which is called from a WCF service. I wish to have this update every minute to display the updated weather data without interfering with user interaction.
I get the following error:
"Must create DependencySource on same Thread as the DependencyObject. "
I have a View Model for getting the weather data asynchronously which inherits from ViewModelBase to handle property changed events. The properties from the ViewModel are bound to the label
ViewModel for weather
public class WeatherDataVM : ViewModelBase
{
private string _windString;
private SolidColorBrush _windState;
private DispatcherTimer _timer;
public WeatherDataVM()
{
_timer = new DispatcherTimer(DispatcherPriority.Render);
_timer.Interval = TimeSpan.FromSeconds(10);
_timer.Tick += async (sender, args) => {await Task.Run(() => GetWindAsync()); };
//_timer.Tick += _timer_Tick;
_timer.Start();
GetWind();
}
private void GetWind()
{
var weatherFromService = Services.Instance.EmptyStackService.GetWeather();
var windSpeed = Convert.ToDouble(weatherFromService.Windspeed);
var maxGust = Convert.ToDouble(weatherFromService.Max_Gust_In_Last_Min);
var windSpeedMPH = Math.Round(windSpeed * 1.15078, 1);
var maxGustMPH = Math.Round(maxGust * 1.15078, 1);
var windString = $"W/S: {windSpeedMPH}({maxGustMPH})";
var windState = new Color();
if (windSpeed >= 40)
windState = Color.FromRgb(255, 64, 64);
else if (windSpeed >= 24)
windState = Color.FromRgb(255, 212, 128);
else
windState = Color.FromRgb(0, 255, 0);
_windState = new SolidColorBrush(windState);
_windString = windString;
}
private async Task GetWindAsync()
{
var weatherFromService = Services.Instance.EmptyStackService.GetWeather();
var windSpeed = Convert.ToDouble(weatherFromService.Windspeed);
var maxGust = Convert.ToDouble(weatherFromService.Max_Gust_In_Last_Min);
var windSpeedMPH = Math.Round(windSpeed * 1.15078, 1);
var maxGustMPH = Math.Round(maxGust * 1.15078, 1);
var windString = $"W/S: {windSpeedMPH}({maxGustMPH})";
var windState = new Color();
if (windSpeed >= 40)
windState = Color.FromRgb(255, 64, 64);
else if (windSpeed >= 24)
windState = Color.FromRgb(255, 212, 128);
else
windState = Color.FromRgb(0, 255, 0);
WindState = new SolidColorBrush(windState);
WindString = windString;
}
public string WindString
{
get { return _windString; }
set
{
if (_windString == value)
return;
_windString = value;
OnPropertyChanged("WindString");
}
}
public SolidColorBrush WindState
{
get { return _windState; }
set
{
if (_windState == value)
return;
_windState = value;
OnPropertyChanged("WindState");
}
}
}
ViewModelBase
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Xaml on view for label
<Label x:Name="lblWeather" Content="{Binding WindString}" Foreground="black" Background="{Binding WindState}" Style="{DynamicResource SmallLabel}" />
code behind view in constructor
lblWeather.DataContext = new WeatherDataVM();
the weather label should change each time the timer ticks. Instead it throws an error.
You can create a brush on a background thread if you freeze it:
var brush = new SolidColorBrush(windState);
brush.Freeze();
WindState = brush;
But it doesn't make much sense to use a DispatcherTimer if you just call Task.Run in the Tick event handler.
Provided that your event handler only creates brushes and don't manipulate any UI elements directly (it certainly shouldn't since it's implemented in a view model), you could use a System.Timer.Timer. Its Elapsed event is queued for execution on a thread pool thread where you can query the service without blocking the UI.
I've got method which I'am calling from main thread. This method is creating a new thread. Code looks like this:
MouseCursorWallObject MouseCursorWall = null;
List<MovingRectangle> MovingRectangles = null;
DrawingImage RenderedImage;
public MainWindow()
{
InitializeComponent();
PrepareObjects();
GameLoopRun();
}
private void GameLoopRun()
{
Thread thread = new Thread(() =>
{
while (true)
{
DateTime dtStart = DateTime.Now;
Events();
Update();
Display();
DateTime dtEnd = DateTime.Now;
TimeSpan ts = dtEnd - dtStart;
if (SkipTicks - ts.TotalMilliseconds >= 0)
{
Thread.Sleep((int)(SkipTicks - ts.TotalMilliseconds));
}
}
});
thread.Start();
}
In Display() method i'am trying update Image control. "Display()" method looks like this:
private void Display()
{
DrawingGroup imageDrawings = new DrawingGroup();
// Drawing main canvas
imageDrawings.Children.Add(DrawingObject(500, 350, 0, 0, new Uri(#"Images\gameCanvas.jpg", UriKind.Relative)));
// Drawing mouse cursor wall
imageDrawings.Children.Add(DrawingObject(MouseCursorWall.Width, MouseCursorWall.Height, MouseCursorWall.GetLocX, MouseCursorWall.GetLocY, MouseCursorWall.DisplayTexture));
for (int i = 0; i < MovingRectangles.Count; i++)
{
MovingRectangle o = MovingRectangles[i];
// Drawing moving object
imageDrawings.Children.Add(DrawingObject(20, 20, o.GetLocX, o.GetLocY, o.TextureUri));
}
if (GamePause == true)
{
}
RenderedImage = new DrawingImage(imageDrawings);
// Image control on main UI thread
renderImage.Dispatcher.Invoke(() =>
{
renderImage.Source = RenderedImage;
});
}
The problem is when I'am trying update Image control using Dispatcher.Invoke I'am receiving error "The calling thread cannot access this object because a different thread owns it". I was trying a lot of different options, and only one works fine:
private void Display()
{
this.Dispatcher.Invoke(() => {
DrawingGroup imageDrawings = new DrawingGroup();
// Drawing main canvas
imageDrawings.Children.Add(DrawingObject(500, 350, 0, 0, new Uri(#"Images\gameCanvas.jpg", UriKind.Relative)));
// Drawing mouse cursor wall
imageDrawings.Children.Add(DrawingObject(MouseCursorWall.Width, MouseCursorWall.Height, MouseCursorWall.GetLocX, MouseCursorWall.GetLocY, MouseCursorWall.DisplayTexture));
for (int i = 0; i < MovingRectangles.Count; i++)
{
MovingRectangle o = MovingRectangles[i];
// Drawing moving object
imageDrawings.Children.Add(DrawingObject(20, 20, o.GetLocX, o.GetLocY, o.TextureUri));
}
if (GamePause == true)
{
}
RenderedImage = new DrawingImage(imageDrawings);
renderImage.Source = RenderedImage;
});
}
Could You explain me why second option of "Display()" method works fine, but the first one throwing exception? What I'am doing wrong?
Both the DrawingImage and the DrawingGroup inherit from DispatcherObject, which means that they need to be accessed from the thread on which they were created. That is why your version where all of the work is invoked back to the dispatcher works correctly.
As pointed out by Brian Reichle, these object also inherit from System.Windows.Freezable, which you can leverage to allow cross thread access to the objects.
I have an array of buttons, like this:
int x = 0, y = 0;
butt2 = new Button[100];
for (int i = 0; i < 100; i++)
{
butt2[i] = new Button();
int names = i;
butt2[i].Name = "b2" + names.ToString();
butt2[i].Location = new Point(525 + (x * 31), 70 + (y * 21));
butt2[i].Visible = true;
butt2[i].Size = new Size(30, 20);
butt2[i].Click += new EventHandler(butt2_2_Click); //problem lies here (1)
this.Controls.Add(butt2[i]);
}
private void butt2_2_Click(object sender, EventArgs e)
{
// want code here
}
I want to change the back color of the button when clicked. I was thinking of passing i to be able to do this:
butt2[i].BackColor = Color.Green;
This should do the trick:
private void butt2_2_Click(object sender, EventArgs e)
{
Button pushedBtn = sender as Button;
if(pushedBtn != null)
{
pushedBtn.BackColor = Color.Green;
}
}
And this holds for most UI events, the 'object sender' parameter refers to the control that 'sent'/'fired' the event.
To learn more about C# event handling, I would start here.
Also, here is a SO question about GUI event handling, answered nicely by Juliet (accepted answer).
Hope this helps.
I'm unsuccessful in making a storyboard in code behind and running it multiple times chained to each other. Somehow, it seems the storyboard keeps in context, and will not reset.
I'm animating several elements, and X number of times I'm recursively running the animation-method, but with different call-back actions in the Completed event. First animation runs fine, but the rest it doesn't animate at all (the completed-event fires).
If I create a StoryBoard in a method and run it, should it not be disposed after it is completed? I'm trying to do storyboard.Remove().
private void SlideLeft(int numberOfStepsToSlide)
{
if (numberOfStepsToSlide < 1) return;
Slide(() => SlideLeft(numberOfStepsToSlide - 1));
}
protected void Slide(Action callBackAfterAnimation = null)
{
var sb = new Storyboard();
sb.FillBehavior = FillBehavior.Stop; //i thought maybe this would fix it, but no
//..
//.. a number of double animations created and added to storyboard
//..
sb.Completed += (sender, e) =>
{
sb.Stop();
sb.Remove();
//..
//..sending message to ViewModel and manipulating values
//..
if (callBackAfterAnimation != null)
callBackAfterAnimation();
};
sb.Begin();
}
Thanks for your time!
Sorry, completely forgot about this question!
You don't want to call Remove - that basically kills the animation dead, by killing all of the animation clocks created to run it...Try something like this instead (quick-and-dirty example):
var win = new Window();
win.Width = 50;
win.Height = 50;
int runCount = 3;
int halfSteps = runCount * 2;
double toWidth = 500.0;
var sb = new Storyboard();
var biggerator = new DoubleAnimation(toWidth, new Duration(TimeSpan.FromSeconds(2)));
sb.Children.Add(biggerator);
Storyboard.SetTarget(biggerator, win);
Storyboard.SetTargetProperty(biggerator, new PropertyPath("Width"));
sb.Completed += (o,e) =>
{
sb.Stop();
halfSteps--;
if(halfSteps <= 0)
{
win.Height = 150;
}
else
{
biggerator.To = biggerator.To == 0 ? toWidth : 0;
sb.Begin();
}
};
sb.Begin();
win.Show();
I'm working on a small project which deals with polling devices to check states of I/O controls. I had implemented a small project dealing with a particular device, but have decided that i would like to eventually implement different devices, and so have moved over to an class : interface approach. This has caused a few problems however, since i moved a lot of code around.
Before i moved the code around and such, i was accessing dynamic form controls by using a delegate as such;
if (result != null)
{
this.Invoke((MethodInvoker)delegate
{
txtOutput1.Text = (result[4] == 0x00 ? "HIGH" : "LOW"); // runs on UI thread
if (result[4] == 0x00)
{
this.Controls["btn" + buttonNumber].BackColor = Color.Green;
}
else
{
this.Controls["btn" + buttonNumber].BackColor = Color.Red;
}
});
}
This worked fine until i moved certain methods to a new class which inherits from an interface. I don't want to just set the dynamic buttons to public, and i'm not sure i can create get;set; for dynamic buttons, considering theres lots of them and are created on startup. Another problem is the this.invoke" command. I believe the invoke command doesn't work unless it's placed on a form...and now it's been moved to a class, so i need to look at another way of doing this.
Does anyone have any ideas as to where i should be heading with this?
EDIT 1:
The program is designed as a monitoring system for hardware devices that handle inputs/outputs. using these i can check if, for example, a door alarm has been triggered and such. The program itself in terms of forms / design is very simple. Currently i have a single form, which generates buttons based on information in a database, for example if there are 10 devices configured, there are 10 buttons. each of these shows green / red dependant on the hardware state.
My main form triggers a thread for each device which monitors it, but because i wished to have multiple types of device i moved them to different classes and an interface which handles all of the common methods. Currently i have a device class, which implements an interface. With regards to this question, i need to now access an instance of the single main form from which i am updating, rather than creating a new instance, so that i can use the new method i created when i moved said logic into the form itself.
EDIT 2:
IdeviceInterface bfdeviceimp = new bf2300deviceimp();
// some other declarations and initialize components
private void btnConnect_Click(object sender, EventArgs e)
{
updateUI();
}
public void updateUI()
{
DBConnector mDBConnector = new DBConnector();
int count = mDBConnector.Count() - 1;
DataTable dataTable = mDBConnector.Select("SELECT * FROM devices");
int x = 12;
int y = 65;
for (int i = 0; i <= count && i < 25; i++)
{
Button btnAdd = new Button();
btnAdd.Text = dataTable.Rows[i]["deviceDescription"].ToString();
btnAdd.Location = new Point(x, y);
btnAdd.Tag = i;
btnAdd.Name = "btn" + i.ToString();
btnAdd.BackColor = Color.Green;
var temp = i + 1;
this.Controls.Add(btnAdd);
this.Controls[btnAdd.Name].MouseClick += (sender, e) =>
{
int index = temp;
generalMethods.generatePopup(sender, e, index);
};
string address = dataTable.Rows[i]["deviceIP"].ToString();
int port = int.Parse(dataTable.Rows[i]["devicePort"].ToString());
ThreadStart workerThread = delegate { start(address, port, i); };
new Thread(workerThread).Start();
x = (x + 75);
if (i != 0 && (i % 5) == 0)
{
x = 12;
y = y + 30;
}
if (i == 25)
{
Button btnPreviousPage = new Button();
btnPreviousPage.Text = "<";
btnPreviousPage.Location = new Point(150, 350);
btnPreviousPage.Tag = "left";
this.Controls.Add(btnPreviousPage);
Button btnNextPage = new Button();
btnNextPage.Text = ">";
btnNextPage.Location = new Point(225, 350);
btnNextPage.Tag = "right";
this.Controls.Add(btnNextPage);
}
}
}
public void start(string address, int port, int i)
{
if (timer == null)
{
timer = new System.Timers.Timer(1000);
timer.Elapsed += delegate(object sender, ElapsedEventArgs e) { timerElapsed(sender, e, address, port, i); };
}
timer.Enabled = true;
// MessageBox.Show("Thread " + i + " Started.");
}
public void timerElapsed(object sender, ElapsedEventArgs e, string address, int port, int i)
{
bfdeviceimp.newconnect(address, port, i);
}
and then finally my device class:
class bf2300deviceimp : IdeviceInterface
{
public void newconnect(string address, int port, int buttonNumber)
{
//send data
byte[] bData = new byte[71];
bData[0] = 240;
bData[1] = 240;
bData[2] = 0;
bData[3] = 1;
bData[68] = 240;
bData[69] = 240;
bData[70] = this.newCalculateCheckSum(bData);
try
{
byte[] result = this.newSendCommandResult(address, port, bData, 72);
//form1.setAlarmColour(result, buttonNumber);
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
Where would you suggest i put the statechanged handler?
You should to use an events-based approach for solving this problem, as is often the case when it comes to passing information between forms. Each of your devices should have a custom event that they define which is fired when the state of that device changes. The event should probably just be defined in the interface for interacting with that device. The form, when it creates the various device classes should subscribe to the event and in the event handler it should update the button/textbox appropriately.
This might be a fair bit to take in if you're not used to this style of programming. Feel free to ask for more details in the comments and I can elaborate on why I did something the way I did or what it actually does.
public Form1()
{
InitializeComponent();
//not sure if this is on initialization or in a button click event handler or wherever.
IDevice device = new SomeDevice();
device.StatusChanged += GetHandlerForDevice(1);
device.DoStuff();
IDevice device2 = new SomeDevice(); //could be another class that implements IDevice
device.StatusChanged += GetHandlerForDevice(2);
device.DoStuff();
}
/// <summary>
/// The handlers for device status changed only vary based on the button number for each one.
/// This method takes a button number and returns an event handler that uses that button number.
/// </summary>
/// <param name="buttonNumber"></param>
/// <returns></returns>
private EventHandler<StatusChangedEventArgs> GetHandlerForDevice(int buttonNumber)
{
//use currying so that the event handler which doesn't have an appropriate signature
//can be attached to the status changed event.
return (sender, args) => device_StatusChanged(sender, args, buttonNumber);
}
private void device_StatusChanged(object sender, StatusChangedEventArgs args, int buttonNumber)
{
this.Invoke((MethodInvoker)delegate
{
txtOutput1.Text = (args.CurrentStatus == IDevice.Status.Green ? "HIGH" : "LOW"); // runs on UI thread
if (args.CurrentStatus == IDevice.Status.Green)
{
this.Controls["btn" + buttonNumber].BackColor = Color.Green;
}
else
{
this.Controls["btn" + buttonNumber].BackColor = Color.Red;
}
});
}
public interface IDevice
{
event EventHandler<StatusChangedEventArgs> StatusChanged;
Status CurrentStatus { get; }
public enum Status
{
Green,
Red
}
void DoStuff();
// rest of interface ...
}
public class StatusChangedEventArgs : EventArgs
{
public IDevice.Status CurrentStatus { get; set; }
//can add additional info to pass from an IDevice to a form if needed.
}
public class SomeDevice : IDevice
{
public event EventHandler<StatusChangedEventArgs> StatusChanged;
private IDevice.Status _currentStatus;
/// <summary>
/// Gets the current status of the device this object represents.
/// When set (privately) it fires the StatusChanged event.
/// </summary>
public IDevice.Status CurrentStatus
{
get { return _currentStatus; }
private set
{
_currentStatus = value;
if (StatusChanged != null)
{
StatusChangedEventArgs args = new StatusChangedEventArgs();
args.CurrentStatus = value;
StatusChanged(this, args);
}
}
}
public void DoStuff()
{
//... do stuff
CurrentStatus = IDevice.Status.Green; //will fire status changed event
}
}
Move all the logic inside a method in the form and use it externally.
In your form create a property
public SynchronizationContext SyncContext { get; set;}
In form constructor add:
this.SyncContext = WindowsFormsSynchronizationContext.Current;
Make interface:
public Interface IChangeClient
{
void Process(<some_type> result); // place your logic here
}
(or somethng like that) and implement it in your Form to change your buttons and text.
Extend your original interface to use (maybe as a parameter) SynchronizationContext and IBackgroundChangeClient.
And than your code would look like this:
if (result != null)
{
oSyncContext.Post(new System.Threading.SendOrPostCallback(
delegate(object state)
{
IBackgroundChangeClient client = (state as object[])[0] as IBackgroundChangeClient
//i dont konw the type of this
var innerResult= (state as object[])[1];
client.Process(innerResult);
}), new object[] { oBackgroundChangeClient, result[4]});
}