I created some test code so I could try and figure out how to use multiple windows in UWP properly. I wanted to see if I could fire an event and have multiple windows update their UI in the event handler. I finally got something working but I'm not entirely sure why it works.
Here's the class that's being created in my Page
public class NumberCruncher
{
private static Dictionary<int, Tuple<CoreDispatcher, NumberCruncher>> StaticDispatchers { get; set; }
static NumberCruncher()
{
StaticDispatchers = new Dictionary<int, Tuple<CoreDispatcher, NumberCruncher>>();
}
public NumberCruncher()
{
}
public event EventHandler<NumberEventArgs> NumberEvent;
public static void Register(int id, CoreDispatcher dispatcher, NumberCruncher numberCruncher)
{
StaticDispatchers.Add(id, new Tuple<CoreDispatcher, NumberCruncher>(dispatcher, numberCruncher));
}
public async Task SendInNumber(int id, int value)
{
foreach (var dispatcher in StaticDispatchers)
{
await dispatcher.Value.Item1.RunAsync(CoreDispatcherPriority.Normal, () =>
{
Debug.WriteLine($"invoking {dispatcher.Key}");
dispatcher.Value.Item2.NumberEvent?.Invoke(null, new NumberEventArgs(id, value));
});
}
}
}
And here's the relevant part of my MainPage code
NumberCruncher numberCruncher;
public MainPage()
{
this.InitializeComponent();
numberCruncher = new NumberCruncher();
numberCruncher.NumberEvent += NumberCruncher_NumberEvent;
}
private async void NumberCruncher_NumberEvent(object sender, NumberEventArgs e)
{
listView.Items.Add($"{e.Id} sent {e.Number}");
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
NumberCruncher.Register(ApplicationView.GetForCurrentView().Id, Window.Current.Dispatcher, numberCruncher);
}
I have a button that creates new views of the MainPage. Then I have another button that calls the SendInNumber() method.
When I navigate to the MainPage I register the Dispatcher for the window and the instance of NumberCruncher. Then when firing the event I use the NumberCruncher EventHandler for that specific Dispatcher.
This works without throwing marshaling exceptions. If I try to use the current class's EventHandler
await dispatcher.Value.Item1.RunAsync(CoreDispatcherPriority.Normal, () =>
{
Debug.WriteLine($"invoking {dispatcher.Key}");
NumberEvent?.Invoke(null, new NumberEventArgs(id, value));
});
I get a marshaling exception when trying to add the item to the listView. However if I maintain the SynchronizationContext in my MainPage and then use SynchronizationContext.Post to update the listView. It works fine
SynchronizationContext synchronizationContext;
public MainPage()
{
this.InitializeComponent();
numberCruncher = new NumberCruncher();
numberCruncher.NumberEvent += NumberCruncher_NumberEvent;
synchronizationContext = SynchronizationContext.Current;
}
private async void NumberCruncher_NumberEvent(object sender, NumberEventArgs e)
{
synchronizationContext.Post(_ =>
{
listView.Items.Add($"{e.Id} sent {e.Number}");
}, null);
}
However this does not work and throws a marshaling exception when trying to update listView.
private async void NumberCruncher_NumberEvent(object sender, NumberEventArgs e)
{
await CoreApplication.GetCurrentView().CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
listView.Items.Add($"{e.Id} sent {e.Number}");
});
}
What is going on here?
Important thing to remember is that when an event is fired, the subscribed methods are called on the same thread as the Invoke method.
If I try to use the current class's EventHandler, I get a marshaling exception when trying to add the item to the listView.
This first error happens because you are trying to fire the event of current class on another Window's dispatcher (dispatcher.Value.Item1). Let's say the event is on Window 1 and the dispatcher.Value.Item1 belongs to Window 2. Once inside the Dispatcher block, you are or the UI thread of Window 2 and firing the NumberEvent of Window 1's NumberCruncher will run the Window 1's handler on the Window 2's UI thread and that causes the exception.
However this does not work and throws a marshaling exception when trying to update listView.
The GetCurrentView() method returns the currently active view. So whichever application view is active at that moment will be the one returned. In your case it will be the one on which you clicked the button. In case you call the Invoke method on the target Window's UI thread, you don't need any additional code inside the NumberEvent handler.
Related
I am using C# and Xamarin. I have two separate classes. One class is essentially the user interface and another class is acting as a custom built generic entry for users to input data and search for results by clicking a button.
Main UI Class:
Class MainPage
{
public MainPage
{
Content = new StackLayout
{
Children =
{
new InputClass // This is my custom built user entry class
{
}.Invoke(ic => ic.Clicked += WhenButtonPressedMethod) // The problem is here, I can't figure out how to call the button within the input class to fire a clicked event.
}
}
}
}
public async void WhenButtonPressedMethod (object sender, EventArgs e)
{
// Supposed to do stuff when the button is pressed
}
InputClass:
public class InputClass : Grid
{
public delegate void OnClickedHandler(object sender, EventArgs e);
public event OnClickHandler Clicked;
public InputClass
{
Children.Add(
new Button {}
.Invoke(button => button.Clicked += Button_Clicked)
)
}
private void Button_Clicked(object sender, EventArgs e)
{
Clicked?.Invoke(this, e);
}
}
The "InputClass" is a grid that holds a title text label, an entry and a button that a user can press to submit and search data. The button in this class is what I'm trying to actually access to invoke/cause a click event so that the method in the main UI class can be called. But, when I try to invoke a click event on the "InputClass" I can't access the button inside of it, I can only access "InputClass" itself which is just a grid with no useful event properties.
Any solutions or ideas?
If you are running into the same problem as mentioned here, follow the code on this page and read through the comments, it covers enough to be able to piece it together. My mistake was attaching Invokes to the wrong objects.
Don't know why fluent Invoke didn't work correctly.
Add the event handlers this way:
public MainPage
{
var ic = new InputClass();
ic.Clicked += WhenButtonPressedMethod;
Content = new StackLayout
{
Children = { ic }
}
}
public InputClass
{
var button = new Button;
button.Clicked += Button_Clicked;
Children.Add(button);
}
I have a simple WPF window with: Loaded="StartTest"
and
<Grid>
<ListBox ItemsSource="{Binding Logging, IsAsync=True}"></ListBox>
</Grid>
In code behind I have in method StartTest:
LogModel LogModel = new LogModel();
void StartTest(object sender, RoutedEventArgs e)
{
DataContext = LogModel;
for (int i = 1; i<= 10; i++)
{
LogModel.Add("Test");
Thread.Sleep(100);
}
}
And class LogModel is:
public class LogModel : INotifyPropertyChanged
{
public LogModel()
{
Dispatcher = Dispatcher.CurrentDispatcher;
Logging = new ObservableCollection<string>();
}
Dispatcher Dispatcher;
public ObservableCollection<string> Logging { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public void Add(string text)
{
Dispatcher.BeginInvoke((Action)delegate ()
{
Logging.Add(text);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Logging"));
});
}
}
Of course the problem is that the UI doesn't update in the loop.
What am I missing?
How can I achieve the UI update?
ObservableCollection already raises the PropertyChanged event when it's modified. You don't have to raise the event in the UI thread either.
Your model can be as simple as :
class LogModel
{
public ObservableCollection<string> Logging { get; } = new ObservableCollection<string>();
public void Add(string text)
{
Logging.Add(text);
}
}
All you need to do is set it as the DataContext of your view, eg :
LogModel model = new LogModel();
public MainWindow()
{
InitializeComponent();
this.DataContext = model;
}
I assume StartTest is a click handler which means it runs on the UI thread. That means it will block the UI thread until the loop finishes. Once the loop finishes the UI will be updated.
If you want the UI to remain responsive during the loop, use Task.Delay instead of Thread.Slepp, eg :
private async void Button_Click(object sender, RoutedEventArgs e)
{
for(int i=0;i<10;i++)
{
await Task.Delay(100);
model.Add("Blah!");
}
}
Update
You don't need to use an ObservableCollection as a data binding source. You could use any object, including an array or List. In this case though you'd have to raise the PropertyChanged event in code :
class LogModel:INotifyPropertyChanged
{
public List<string> Logging { get; } = new List<string>();
public event PropertyChangedEventHandler PropertyChanged;
public void Add(string text)
{
Logging.Add(text);
PropertyChanged.Invoke(this,new PropertyChangedEventArgs("Logging"));
}
}
This will tell the view to load all the contents and display them again. This is perfectly fine when you only want to display data loaded eg from the database without modifying them, as it makes mapping entities to ViewModels a lot easier. In this case you only need to update the view when a new ViewModel is attached as a result of a command.
This is not efficient when you need to update the coolection though. ObservableCollection implements the INotifyCollectionChanged interface that raises an event for each change. If you add a new item, only that item will be rendered.
On the other hand you should avoid modifying the collection in tight loops because it will raise multiple events. If you load 50 new items, don't call Add 50 times in a loop. Create a new ObservableCollection, replace the old one and raise the PropertyChanged event, eg :
class LogModel:INotifyPropertyChanged
{
public ObservableCollection<string> Logging { get; set; } = new ObservableCollection<string>();
public event PropertyChangedEventHandler PropertyChanged;
public void Add(string text)
{
Logging.Add(text);
PropertyChanged.Invoke(this,new PropertyChangedEventArgs("Logging"));
}
public void BulkLoad(string[] texts)
{
Logging = new ObservableCollection<string>(texts);
PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Logging"));
}
}
The explicit implementation is still needed because the Logging property is getting replaced and can't raise any events itself
The reason why the UI is not updated in the loop is a call to Dispatcher.BeginInvoke. This places a new DispatcherOperation in the dispatcher queue. But your loop is already a dispatcher operation, and it continues on the Dispatcher's thread. So all the operations you queue will be executed after the loop's operation is finished.
Maybe you wanted to run the StartTest on a background thread? Then, the UI will update.
By the way, don't block the Dispatcher's thread with Thread.Sleep. It prevents the Dispatcher from doing its things as smoothly as possible.
It is the DoEvents thing, overhere:
public static void DoEvents()
{
Application.Current.Dispatcher.Invoke(DispatcherPriority.Background,
new Action(delegate { }));
}
or even the perhaps better https://stackoverflow.com/a/11899439/138078.
Of course the test should be written differently in a way which does not require it.
I have a C# main form that opens up C# sub forms in separate tabs. Each tab is simply an instance of the same sub form so the tab code is like:
SubForm sf = new SubForm();
TabPage tp = new TabPage();
tp.Controls.Add(sf);
tabControl.TabPages.Add(tp);
There can be n tabs and subforms. Then each new subform instance has a delegate to handle external event updates, like so:
public partial class SubForm : Form
{
... form setup ...
internal void DoStuff(value v)
{
if (InvokeRequired)
{
// Generic Action delegate
Invoke(new Action<string, string>(DoStuff), value);
return;
}
myLabel.Text = value;
Show();
}
}
Click the subscribe button and there's a Geode registration to specific keys in the subform, and the delegate is passed as an event handler:
private void button_Click(object sender, EventArgs e)
{
new Geode().RegisterMyListener(cache, key, DoStuff);
}
When the Geode key value is updated then the update is handled.
This is working fine for the 1st subform. Then with a 2nd or 3rd subform all the Geode subscriptions to each subform's keys are updating, but all the updates are being handled only by the most recently instantiated subform's delegate. I had not expected that to happen because doesn't each new subform instance have its own stack with a new delegate?
UPDATE: Once a 2nd key is registered with Geode RegisterMyListener like this:
region.GetSubscriptionService().RegisterKeys(s);
region.AttributesMutator.SetCacheListener(new Listener<string, string>(DoStuff));
then every event update references the latest DoStuff delegate and never a previous one. So is a Geode listener a static register? I am looking to be able to subscribe to separate keys with many instances of the same Listener. Is that possible? Or am I going to need multiple listeners for multiple subforms?
You can do it with extension method like this:
public static class Extensions
{
public static void InvokeAction(this Control control, Action action)
{
if (control.InvokeRequired)
{
control.Invoke(new MethodInvoker(() => { action(); }));
}
else
{
action();
}
}
}
Usage:
public partial class SubForm : Form
{
public void SetExampleText(string text)
{
this.InvokeAction(() => { this.ExampleTextBox.Text = text; })
}
}
I have Portable project in Visual Studio 2015 with a ListView that gets populated with some data via an API call through the following function refreshData:
async Task refreshData()
{
myListView.BeginRefresh();
var apiCallResult = await App.Api.myApiGetCall();
myListView.ItemsSource = apiCallResult;
myListView.EndRefresh();
}
refreshData() is called in
protected override void OnAppearing()
{
base.OnAppearing();
refreshData();
}
Everything is working fine except on Android where the refresh indicator is not stopping or disappearing on EndRefresh() when the page is initially loaded. The page is in a TabbedPage so I can go to a different tab and then return to this page and the refresh indicator properly starts and stops with completion of my API call.
Why is refresh is not stopping when the page initially loads on Android? Any help would be appreciated.
Note: This works perfectly fine when I run on iOS.
So far I've tried:
replacing myListView.BeginRefresh() with myListView.IsRefreshing = true and myListView.EndRefresh() with myListView.IsRefreshing = false
Using Device.BeginInvokeOnMainThread(() => {//update list and endRefresh}).
Using async void refreshData() instead of async Task refreshData().
Personally I can get this problem when I start ListView refreshing in the Page Contructor and stop it after the data is loaded. Sometimes (quite often) Xamarin.Forms ListView doesn't cancel refreshing animation.
I believe you faced with a quite common issue with Android SwipeRefreshLayout: it may not stop refreshing animation after setRefreshing(false) called. Native Android developers use the following approach:
swipeRefreshLayout.post(new Runnable() {
#Override
public void run() {
mSwipeRefreshLayout.setRefreshing(refreshing);
}
});
Interestingly, Xamarin.Forms uses this approach when it sets initial refreshing status (code); however, it is not enough. You need a custom renderer:
public class ExtendedListViewRenderer : ListViewRenderer
{
/// <summary>
/// The refresh layout that wraps the native ListView.
/// </summary>
private SwipeRefreshLayout _refreshLayout;
public ExtendedListViewRenderer(Android.Content.Context context) : base(context)
{
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_refreshLayout = null;
}
base.Dispose(disposing);
}
protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
{
base.OnElementChanged(e);
_refreshLayout = (SwipeRefreshLayout)Control.Parent;
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == ListView.IsRefreshingProperty.PropertyName)
{
// Do not call base method: we are handling it manually
UpdateIsRefreshing();
return;
}
base.OnElementPropertyChanged(sender, e);
}
/// <summary>
/// Updates SwipeRefreshLayout animation status depending on the IsRefreshing Element
/// property.
/// </summary>
protected void UpdateIsRefreshing()
{
// I'm afraid this method can be called after the ListViewRenderer is disposed
// So let's create a new reference to the SwipeRefreshLayout instance
SwipeRefreshLayout refreshLayoutInstance = _refreshLayout;
if (refreshLayoutInstance == null)
{
return;
}
bool isRefreshing = Element.IsRefreshing;
refreshLayoutInstance.Post(() =>
{
refreshLayoutInstance.Refreshing = isRefreshing;
});
}
}
Try this:
async void refreshData()
{
Device.BeginInvokeOnMainThread(() => {
myListView.BeginRefresh();
var apiCallResult = await App.Api.myApiGetCall();
myListView.ItemsSource = apiCallResult;
myListView.EndRefresh();
});
}
Apparently, there is no need for "Task" anymore.
If the error occurs only in Android, it's possible that it's just the way Android handles threads. It does not allow threads to change visual elements directly. Sometimes when we try to do that it throws a silent exception and the code action have no practical effect.
You need to follow MVVM Pattern
On your ViewModel you need to:
Implement INotifyPropertyChanged
Define properties like:
private bool _IsRefreshing;
public bool IsRefreshing
{
get { return _IsRefreshing; }
set { SetProperty(ref _IsRefreshing, value; }
/*So every time the property changes the view is notified*/
}
Define the method that fetch your data, in your case refreshData()
Toggle the IsRefreshing true/false when needed
On your Page you need to:
Bind the listview itemSource to a VM property with the SetPropertyValue
Bind ListView.IsRefreshing to ViewModel's IsRefreshing:
MyListView.SetBinding<MyViewModel>(ListView.IsRefreshing, vm => vm.IsRefreshing);
Here is a great article talking about INotifyPropertyChanged
I have this requirement where I need to show a custom popUp as a page overlay. This custom PopUp is random and can show up on any page (based on some logic). I have registered the OnBackKeyPress event (for the current page) in this custom PopUp class.
But because almost all the pages of app also has an OnBackKeyPress method defined (again as per business requirement), the new event registration (in the custom PopUp class) takes place after the previous one. So on pressing the hardware back key, the OnBackKeyPress method written in pages is getting called first followed by the OnBackKeyPress method written in custom PopUp class. I need to avoid calling OnBackKeyPress method written in pages.
Solutions not acceptable :
make the new popUp control as a PhoneApplicationPage (we need some transparency where we can show the current page's data)
Put check on OnBackKeyPress method on all pages for the popUp (so many pages in ap!)
Extend PhoneApplicationPage and write a new OnBackKeyPress method to handle the same (no no!)
Here is an example of how to subscribe an event before already subscribed handlers:
class EventTesterClass // Simple class that raise an event
{
public event EventHandler Func;
public void RaiseEvent()
{
Func(null, EventArgs.Empty);
}
}
static void subscribeEventBeforeOthers<InstanceType>(InstanceType instance, string eventName, EventHandler handler) // Magic method that unsubscribe subscribed methods, subscribe the new method and resubscribe previous methods
{
Type instanceType = typeof(InstanceType);
var eventInfo = instanceType.GetEvent(eventName);
var eventFieldInfo = instanceType.GetField(eventInfo.Name,
BindingFlags.NonPublic |
BindingFlags.Instance |
BindingFlags.GetField);
var eventFieldValue = (System.Delegate)eventFieldInfo.GetValue(instance);
var methods = eventFieldValue.GetInvocationList();
foreach (var item in methods)
{
eventInfo.RemoveEventHandler(instance, item);
}
eventInfo.AddEventHandler(instance, handler);
foreach (var item in methods)
{
eventInfo.AddEventHandler(instance, item);
}
}
static void Main(string[] args)
{
var evntTest = new EventTesterClass();
evntTest.Func += handler_Func;
evntTest.Func += handler_Func1;
evntTest.RaiseEvent();
Console.WriteLine();
subscribeEventBeforeOthers(evntTest, "Func", handler_Func2);
evntTest.RaiseEvent();
}
static void handler_Func(object sender, EventArgs e)
{
Console.WriteLine("handler_Func");
}
static void handler_Func1(object sender, EventArgs e)
{
Console.WriteLine("handler_Func1");
}
static void handler_Func2(object sender, EventArgs e)
{
Console.WriteLine("handler_Func2");
}
Output will be:
handler_Func
handler_Func1
handler_Func2
handler_Func
handler_Func1