Well, i'm working in a asp.net 3.5 site.
I have set an Observer like this:
public delegate void ActionNotification();
protected Dictionary<string, List<ActionNotification>> Observers
{
get
{
Dictionary<string, List<ActionNotification>> _observers = Session["Observers"] as Dictionary<string, List<ActionNotification>>;
if (_observers == null)
{
_observers = new Dictionary<string, List<ActionNotification>>();
Observers = _observers;
}
return _observers;
}
set
{
Session["Observers"] = value;
}
}
public void Attach(string actionName, ActionNotification observer)
{
if (!Observers.ContainsKey(actionName))
{
Observers.Add(actionName, new List<ActionNotification>());
}
Observers[actionName].Add(observer);
}
public void Detach(string actionName, ActionNotification observer)
{
if (Observers.ContainsKey(actionName))
{
Observers[actionName].Remove(observer);
}
}
public void DetachAll(string actionName)
{
if (Observers.ContainsKey(actionName))
{
Observers.Remove(actionName);
}
}
public void Notify(string action)
{
if (Observers.ContainsKey(action))
{
foreach (ActionNotification o in Observers[action])
{
o.Invoke();
}
}
}
I use the observer like this:
//Esta es llamada al notify con cierto action
protected void btnNext_Click(object sender, ImageClickEventArgs e)
{
Notify("Next");
}
//Y este es el register del Listener
Attach("Next", new ActionNotification(NextButton_Click));
If before the o.Invoke(); for example i change the page title to "Hello".
And inside the "NextButton_Click" I set it to "Goodbye", after the NextButton_Click finish, the Title goes back to "Hello"...
Any idea why?
I think problem is that the "Page" in your NextButton_Click event is not the same page as the page you set the title to "Hello" on. Because you are passing around events in the session when the event is raised the object is acts on is no longer in scope. You can recreate it with the following code (which is using EventHandlers, but they are basically the same as what you have outlined in your code)
protected void Page_Load(object sender, EventArgs e)
{
this.Page.Title = "test";
//Store it in your session...seems like a weird thing to do given how your page should be stateless, so I would think about what you are
//trying to do a bit more carefully. You don't want to call an event handler such as test below from another page in your asp.net app.
Dictionary<string, EventHandler> myEvents = null;
if (Session["Invokers"] == null)
{
myEvents = new Dictionary<string, EventHandler>();
Session["Invokers"] = myEvents;
}
else
{
myEvents = Session["Invokers"] as Dictionary<string, EventHandler>;
}
//If the event handler key is not in there then add it
if (myEvents.ContainsKey("buttonClickOnPageDefault") == false)
{
//Subscribe to event (i.e. add your method to the invokation list
this.TestEvent += new EventHandler(test);
myEvents.Add("buttonClickOnPageDefault", this.TestEvent);
}
else
{
//if it does contain this key then you may already be subscribed to event, so unsubscribe in case and then resubscribe...you could
//probably do this more elegantly by looking at the vales in the GetInvokationList method on the eventHandler
//Wire up the event
this.TestEvent -= new EventHandler(test);
this.TestEvent += new EventHandler(test);
}
//Resave the dictionary.
Session["Invokers"] = myEvents;
}
void test(object o, EventArgs e)
{
this.Page.Title = "testEvent";
}
public event EventHandler TestEvent;
protected void Button1_Click(object sender, EventArgs e)
{
if (Session["Invokers"] != null)
{
Dictionary<string, EventHandler> myEvents = (Dictionary<string, EventHandler>)Session["Invokers"];
if (myEvents.ContainsKey("buttonClickOnPageDefault"))
{
EventHandler ev = myEvents["buttonClickOnPageDefault"];
ev(null, EventArgs.Empty);
}
}
}
If you put the above code in an asp.net page it will never change page title, but if you put a breakpoint in the Test method you will see it being hit. The reason is that its being hit in a different page (and that page is out of scope and may not be garbage collected as your event still has a reference to it, so this could cause a memory leak...be careful with it!). Really you probably shouldn't be using your events this way (at least not to act on a page...maybe it has some utility for domain objects). Note that the following will work (as its acting on the same page)
protected void Page_Load(object sender, EventArgs e)
{
this.Page.Title = "test";
//Store it in your session...seems like a weird thing to do given how your page should be stateless, so I would think about what you are
//trying to do a bit more carefully. You don't want to call an event handler such as test below from another page in your asp.net app.
this.TestEvent += new EventHandler(test);
Session["Invoker"] = this.TestEvent;
}
void test(object o, EventArgs e)
{
this.Page.Title = "testEvent";
}
public event EventHandler TestEvent;
protected void Button1_Click(object sender, EventArgs e)
{
if (Session["Invoker"] != null)
{
EventHandler ev = (EventHandler)Session["Invoker"];
ev(null, EventArgs.Empty);
}
}
Hope that gives you some pointers to where your problem might be.
Related
I have 'BasePage' class which is base class for all other pages in my project.
In initialization, I'm adding EventHandler for 'SystemNavigationManager' for event 'BackRequest'. For some reason that line id causing 'AccessViolationException' when XAML designer is trying to render XAML of class that extends 'BasePage'
I'm not familiar with UWP, so I'll be very grateful for tips.
BasePage
public class BasePage: Page {
internal string title = "";
internal HeaderView headerView;
public BasePage() {
this.Loaded += BasePage_Loaded;
// FIXME: For some reason if this line is uncommented then xaml designer fails.
SystemNavigationManager.GetForCurrentView().BackRequested += BasePage_BackRequested;
}
private void BasePage_BackRequested(object sender, BackRequestedEventArgs e) {
bool handled = e.Handled;
this.BackRequested(ref handled);
e.Handled = handled;
}
private void BackRequested(ref bool handled) {
//Get a hold of the current frame so that we can inspect the app back stack.
if (this.Frame == null)
return;
// Check to see if this is the top-most page on the app back stack.
if (this.Frame.CanGoBack && !handled) {
// If not, set the event to handled and go back to the previous page in the app.
handled = true;
this.Frame.GoBack();
}
}
private void setupPageAnimation() {
TransitionCollection collection = new TransitionCollection();
NavigationThemeTransition theme = new NavigationThemeTransition();
var info = new ContinuumNavigationTransitionInfo();
theme.DefaultNavigationTransitionInfo = info;
collection.Add(theme);
this.Transitions = collection;
}
private void BasePage_Loaded(object sender, Windows.UI.Xaml.RoutedEventArgs e) {
setupPageAnimation();
}
}
SOLUTION
Just like Ivan said, final code looks like this. Without of a trace of bug.
BasePage
public BasePage() {
this.Loaded += BasePage_Loaded;
}
protected override void OnNavigatedTo(NavigationEventArgs e) {
base.OnNavigatedTo(e);
SystemNavigationManager.GetForCurrentView().BackRequested += BasePage_BackRequested;
}
protected override void OnNavigatedFrom(NavigationEventArgs e) {
base.OnNavigatedFrom(e);
SystemNavigationManager.GetForCurrentView().BackRequested -= BasePage_BackRequested;
}
You shouldn't subscribe to back events on constructor but OnNavigatedTo and unsubscribe in OnNavigatedFrom. Even if it didn't crash it would cause a lot of problems because your back logic would be activated on all previous pages when you press the back button.
I have registered a handler with the HardwareButtons.BackPressed event, performed some logic and then set the handled property in the args to true if it applies. The handler runs through without any issue, and the Handled property gets set. The phone still navigates back outside of the app. Am I misunderstanding how to use the event?
Page
public sealed partial class FirstRunPage : VisualStateAwarePage
{
public FirstRunPage()
{
this.InitializeComponent();
#if WINDOWS_PHONE_APP
HardwareButtons.BackPressed += (sender, args) =>
{
bool isHandled = false;
Action handledCallback = () => isHandled = true;
var state = new Dictionary<string, object> { { "Callback", handledCallback } };
((INavigationAware)this.DataContext).OnNavigatedTo("Back", NavigationMode.Back, state);
args.Handled = isHandled;
};
#endif
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
}
}
View model.
public override void OnNavigatedTo(object navigationParameter, NavigationMode navigationMode, Dictionary<string, object> viewModelState)
{
if (navigationParameter == null || !navigationParameter.ToString().Equals("Back"))
{
return;
}
if (!viewModelState.ContainsKey("Callback"))
{
return;
}
var callback = (Action)viewModelState["Callback"];
// If the user is new, then we set it to false and invoke our callback.
if (this.IsNewUser)
{
this.IsNewUser = false;
callback();
}
else
{
return;
}
}
Update
I have modified my FirstRunPage to subscribe and unsubscribe as recommended by #Martin but it still closes the app.
public sealed partial class FirstRunPage : VisualStateAwarePage
{
public FirstRunPage()
{
this.InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
#if WINDOWS_PHONE_APP
HardwareButtons.BackPressed += HardwareBack_OnPressed;
#endif
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
#if WINDOWS_PHONE_APP
HardwareButtons.BackPressed -= HardwareBack_OnPressed;
#endif
}
private void HardwareBack_OnPressed(object sender, BackPressedEventArgs e)
{
Action handledCallback = () => e.Handled = true;
var state = new Dictionary<string, object> { { "Callback", handledCallback } };
((INavigationAware)this.DataContext).OnNavigatedTo("Back", NavigationMode.Back, state);
}
}
With the help of #yasen I was able to get this resolved. The issue stems from the fact that the Prism library has your App.xaml.cs inherit from MvvmAppbase, which intercepts the BackPressed event.
To resolve this, I overrode the MvvmAppBase OnHardwareButtonsBackPressed and added a bit of logic to handle it.
My view model and view both implement a new interface called INavigateBackwards and they're used like this:
View Model
public bool CanNavigateBack()
{
// If the new user is true, then we can't navigate backwards.
// There isn't any navigation stack, so the app will die.
bool canNavigate = !this.IsNewUser;
// Disable the new user mode.
this.IsNewUser = false;
// Return so that the view can return to it's sign-in state.
return canNavigate;
}
View
public sealed partial class FirstRunPage : VisualStateAwarePage, INavigateBackwards
{
private INavigateBackwards ViewModel
{
get
{
return (INavigateBackwards)this.DataContext;
}
}
public FirstRunPage()
{
this.InitializeComponent();
}
public bool CanNavigateBack()
{
return ViewModel.CanNavigateBack();
}
}
Then in the MvvmAppBase subclass, I determine if I need to handle the navigation or not.
MvvmAppBase child
protected override void OnHardwareButtonsBackPressed(object sender, BackPressedEventArgs e)
{
var page = (Page)((Frame)Window.Current.Content).Content;
if (page is INavigateBackwards)
{
var navigatingPage = (INavigateBackwards)page;
if (!navigatingPage.CanNavigateBack())
{
e.Handled = true;
return;
}
}
base.OnHardwareButtonsBackPressed(sender, e);
}
This allows my single view to have multiple states and the user to navigate back from one state to the previous without navigating to an entirely new view.
The reason why your application closes is that the same handler is called more than just once. First handler sets the Handled property to true, but any other subsequent call for the same event fire sets it back to false.
To illustrate it, try this:
public sealed partial class FirstRunPage : VisualStateAwarePage
{
public FirstRunPage()
{
// ...
InitializeComponent();
HardwareButtons.BackPressed += HardwareButtons_BackPressed;
HardwareButtons.BackPressed += HardwareButtons_BackPressed;
}
void HardwareButtons_BackPressed(object sender, BackPressedEventArgs e)
{
bool isHandled = false;
Action handledCallback = () => isHandled = true;
var state = new Dictionary<string, object> { { "Callback", handledCallback } };
((INavigationAware)this.DataContext).OnNavigatedTo("Back", NavigationMode.Back, state);
args.Handled = isHandled;
};
}
And set breakpoint to last line of the handler code.
To avoid it, assign your handler in the OnNavigatedTo method of your FirstRunPage, and unregister the handler in OnNavigatedFrom.
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
HardwareButtons.BackPressed += HardwareButtons_BackPressed;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
HardwareButtons.BackPressed -= HardwareButtons_BackPressed;
}
I got an enum:
public enum sprog{
dansk,
svensk,
norsk
}
In a method I would raise an event and use the enum to carry information:
public delegate void BrugerSprogChanged(Object sender, Sprog sprog);
class clazz
{
public event BrugerSprogChanged brugerSprogChanced;
public clazz(){}
private void comboBoxDokumentSprog_SelectedIndexChanged(object sender, EventArgs e)
{
Sprog sprog = FindSprog((string)((ComboBox)sender).SelectedItem);
dokumentSprogChanged(this, sprog); // <- here we have the problem
}
}
When the code shall raise the event I get an exception when dokumentSprogChanged(this, sprg) is called:
*"NullReferenceException was unhandled by user code
Object reference not set to an instance of an object"
"this" and "sprog" are not null.
Any suggestions?
The easy way is to drop the unem and use an integer/string instead, but then I end up with some ugly code.
Normally to call an event you have to check if it's handler is not null:
var handler = dokumentSprogChanged; // take a local reference
if (handler != null)
{
dokumentSprogChanged(this, sprog);
}
This way you can raise it safely.
EDIT
You need to register the event like this:
public event BrugerSprogChanged brugerSprogChanced;
....
brugerSprogChanced += class_brugerSprogChanced;
....
void class_brugerSprogChanced(object sender, EventArgs e)
{
// handle there
}
Try this:
class clazz
{
public event BrugerSprogChanged brugerSprogChanced;
public clazz(){}
private void comboBoxDokumentSprog_SelectedIndexChanged(object sender, EventArgs e)
{
Sprog sprog = FindSprog((string)((ComboBox)sender).SelectedItem);
if (dokumentSprogChanged != null)
{
dokumentSprogChanged(this, sprog); // <- here we have the problem
}
}
}
I need to pass data with an event. Currently, when receiving more data (via comport), the event will fire but the previous event (&data) is not handled yet, so the data gets overwritten.
How can I handle the event &data in a safe way? I have multiple events like this (15x), so I am not sure if using a queue for the data is the best way or pass data along with the event (like S.O. item 4215845).
Example (this example is with a string, but I also use arrays, bools etc):
At the 'sender' side (class1):
public event EventHandler evDiaStringMessage = delegate { };
private void evDiaStringMessageEvent()
{
evDiaStringMessage(this, new EventArgs());
}
private static string _DiaString;
public string DiaString
{
get { return _DiaString; }
set { _DiaString = value; }
}
DiaString contains the data and gets overwritten when evDiaStringMessage is fired too soon.
At the 'receiver / GUI' side (class2):
dia.evDiaStringMessage += new EventHandler(dia_evDiaStringMessage);
private delegate void dia_evDiaStringMessageCallBack(object sender, EventArgs e);
void dia_evDiaStringMessage(object sender, EventArgs e)
{
if (this.InvokeRequired)
{
this.BeginInvoke(new dia_evDiaStringMessageCallBack(dia_evDiaStringMessage), new object[] { sender, e});
}
else
{
frmcomm.CommTextBox("Receiver message: " + dia.DiaString + "\r\n", Color.Red);
}
}
dia.DiaString does not contain the expected data (previous data), but all events are 'received'.
Your help is very much appreciated! Even more with an example!
Edit:
I changed the code to:
At the 'sender' side (class1):
public event EventHandler<DiaStringEventArgs> evDiaStringMessage ;
public class DiaStringEventArgs : EventArgs
{
public string DiaString { get; set; }
}
private void evDiaStringMessageEvent(DiaStringEventArgs e)
{
EventHandler<DiaStringEventArgs> handler = evDiaStringMessage;
if (handler != null)
handler(this, e);
}
...
private void PrepareDataAndFireEvent()
{
DiaStringEventArgs args = new DiaStringEventArgs();
args.DiaString = ByteToString(data);
evDiaStringMessageEvent(args);
}
At the 'receiver / GUI' side (class2):
dia.evDiaStringMessage += new EventHandler<ifDiA10.DiaStringEventArgs>(dia_evDiaStringMessage);
private delegate void dia_evDiaStringMessageCallBack(object sender, ifDiA10.DiaStringEventArgs e);
void dia_evDiaStringMessage(object sender, ifDiA10.DiaStringEventArgs e)
{
if (this.InvokeRequired)
{
this.BeginInvoke(new dia_evDiaStringMessageCallBack(dia_evDiaStringMessage), new object[] { sender, e});
}
else
{
frmcomm.CommTextBox("Receiver message: " + e.DiaString + "\r\n", Color.Red);
}
}
You can store your data in a custom EventArgs class:
public class ReceivedDataEventArgs : EventArgs
{
// Add the properties you require
}
The event is defined like so:
public event EventHandler<ReceivedDataEventArgs> ReceivedData;
Your handler will take in an instance your class instead of the EventArgs object, and hence you'll have the correct data.
You should pass the value of dia.DiaString when the event is raised rather than reading it back when the event is handled.
You can do this by extending the EventArgs class and creating custom properties.
If you need an example let me know.
Let me show the code first.
WCF servicecontract function:
public List<VenueData> GetVenues()
{
List<VenueData> listOfVenues = new List<VenueData>();
string connString = #"....";
DataContext dc = new DataContext(connString);
Table<VenueData> venues = dc.GetTable<VenueData>();
listOfVenues = (from v in venues
select v).ToList();
return listOfVenues;
}
VenueViewModel.cs
public class VenueViewModel : ViewModelBase
{
private VenueData _venue;
private ObservableCollection<VenueData> _venues = new ObservableCollection<VenueData>();
public VenueData Venue
{
get
{
return _venue;
}
set
{
if (_venue != value)
{
_venue = value;
OnNotifyPropertyChanged("Venue");
}
}
}
public ObservableCollection<VenueData> Venues
{
get
{
return _venues;
}
set
{
if (_venues != value)
{
_venues = value;
OnNotifyPropertyChanged("Venues");
}
}
}
public void GetAllVenues()
{
TicketOrderWcfClient toClient = new TicketOrderWcfClient();
toClient.GetVenuesCompleted += new EventHandler<GetVenuesCompletedEventArgs>(toClient_GetVenuesCompleted);
toClient.GetVenuesAsync();
}
void toClient_GetVenuesCompleted(object sender, GetVenuesCompletedEventArgs e)
{
if (e.Error == null)
Venues = e.Result;
}
}
MainPage.xaml(view)
public MainPage()
{
InitializeComponent();
Loaded += new RoutedEventHandler(MainPage_Loaded);
}
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
VenueViewModel vvm = new VenueViewModel();
vvm.GetAllVenues();
MessageBox.Show(vvm.Venues.Count.ToString());
}
Well this is most of the code. The problem is that in the MainPage_Loaded event vvm.GetAllVenues() will not populate the Venues ObservableCollection. The MessageBox will show 0. I tested the wcf service is good, also fiddler showed the soap fine. Also if i call the wcf service in the MainPage_Loaded event, then it will work. See below:
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
TicketOrderWcfClient toClient = new TicketOrderWcfClient();
toClient.GetVenuesCompleted += new EventHandler<GetVenuesCompletedEventArgs>(toClient_GetVenuesCompleted);
toClient.GetVenuesAsync();
}
void toClient_GetVenuesCompleted(object sender, GetVenuesCompletedEventArgs e)
{
if(e.Error == null)
{
VenueViewModel vvm = new VenueViewModel();
vvm.Venues = e.Result;
MessageBox.Show(vvm.Venues.Count.ToString());
}
}
This time MessageBox will show 3, which is good because there are 3 records in the db. So it looks like there is a problem between the View and the ViewModel. I suspect i am missing a pretty basic thing here. Also note that i know this is not true MVVM, but i have to accomplish this program this way.
I hope my explanation is clear, thank you for your help.
In your first approach, which does not work, the flow of the code:
MessageBox.Show(vvm.Venues.Count.ToString());
is not connected with the termination of your asynchronous call of the WCF method! In other word, you show the mbox, but you do not make sure that the asynchronous call has terminated.
Of course, you had assigned a delegate to GetVenuesCompleted event, but the call of toClient.GetVenuesAsync() is an asynchronous call, which means that when invoked it does not wait for the result (termination). So when you call:
vvm.GetAllVenues();
Then
MessageBox.Show(vvm.Venues.Count.ToString());
is invoked faster then your toClient_GetVenuesCompleted delegate.
Your second approach works because you show the messagebox when the asynchronous method is completed (in your GetVenues callback).
In my opinion, you can repair it for example by adding a new event GetAllVenuesCompleted in VenueViewModel which will be fired at the end of the toClient_GetVenuesCompleted delegate. In other words, I would pass this event further. Additionally I would add a comment that GetAllVenues is an asynchronous method.
I guess the problem is, that you are creating new VM.
void toClient_GetVenuesCompleted(object sender, GetVenuesCompletedEventArgs e)
{
if(e.Error == null)
{
VenueViewModel vvm = new VenueViewModel();
vvm.Venues = e.Result;
MessageBox.Show(vvm.Venues.Count.ToString());
}
}
I think this code works. You create new VenueViewModel, fill it with data and that is all. This VM is never used again.
The same rules for MainPage.xaml sample.
You should create global variable of VenuViewModel in constructor/loaded handler and call its GetAllVenues in Loaded event