First off, I'm using Xamarin Studio 6.1.3 in case that makes any difference.
I'm creating a simple app and want a login form to appear as a sheet. I followed the Xamarin tutorial for creating sheets (https://developer.xamarin.com/guides/mac/user-interface/working-with-dialogs/#Creating_a_Custom_Sheet) but am running into an issue.
Per the tutorial, I have created a class:
using System;
using Foundation;
using AppKit;
namespace SampleProject
{
public partial class UserLoginController : NSViewController
{
private NSViewController _presentor;
public string Username
{
get { return TxtUsername.StringValue; }
set { TxtUsername.StringValue = value; }
}
public string Password
{
get { return TxtPassword.StringValue; }
set { TxtPassword.StringValue = value; }
}
public NSViewController Presentor
{
get { return _presentor; }
set { _presentor = value; }
}
public UserLoginController(IntPtr handle) : base(handle)
{
}
private void CloseDialog()
{
Presentor.DismissViewController(this);
}
partial void BtnCancelClick(NSObject sender)
{
RaiseDialogCanceled();
CloseDialog();
}
partial void BtnLoginClick(NSObject sender)
{
RaiseDialogAccepted();
CloseDialog();
}
public EventHandler DialogAccepted;
internal void RaiseDialogAccepted()
{
if (this.DialogAccepted != null)
this.DialogAccepted(this, EventArgs.Empty);
}
public EventHandler DialogCanceled;
internal void RaiseDialogCanceled()
{
if (this.DialogCanceled != null)
this.DialogCanceled(this, EventArgs.Empty);
}
}
}
And I have added an override for PrepareForSegue in my ViewController class:
public override void PrepareForSegue(NSStoryboardSegue segue, NSObject sender)
{
base.PrepareForSegue(segue, sender);
switch (segue.Identifier)
{
case "UserLoginSegue":
UserLoginController loginSheet = segue.DestinationController as UserLoginController;
loginSheet.Username = ""; //This line throws NullReferenceException unless I set a breakpoint and expand loginSheet.Base before allowing this line to execute.
loginSheet.Password = "";
loginSheet.Presentor = this;
loginSheet.DialogAccepted += (object s, EventArgs e) => { Console.WriteLine("OK Clicked"); };
loginSheet.DialogCanceled += (object s, EventArgs e) => { Console.WriteLine("Cancel Clicked"); };
break;
}
}
See the comment in the above code block. I basically set a breakpoint on that line and when it triggers, I inspect the loginSheet object. If I expand the Base object to inspect it and then continue execution, everything works as expected. If I don't, I get a NullReferenceException whenever code tries to access any fields/properties/methods in the UserLoginController class.
I am completely baffled as to why this is happening. I set a breakpoint in the constructor of UserLoginController and verified it is being called with a handle and that the base constructor should be called as well.
I've read through the tutorial several times and don't see anything that I'm missing. Can't seem to find anybody else having the same problem.
My ultimate question is: What can I do to make the code work as expected?
For the sake of learning (which may shed light on the problem): What is going on behind the scenes when I inspect the base object of my UserLoginController class while debugging?
When you expand an object in the debugger, all properties not marked up with certain attributes are read via reflection so that we can display them in the IDE.
Maybe one of those properties has a side effect? You should be able to reproduce the effect using reflection, then bisect the property list to see who's effecting your behavior.
The solution turned out to be that I need to check the View property of my UserLoginController.
I added the following line:
var theView = loginSheet.View;
and everything works as expected. I have yet to dig into the View property to see what it is doing behind the scenes that makes everything work.
Here is the modified, working PrepareForSegue override method:
public override void PrepareForSegue(NSStoryboardSegue segue, NSObject sender)
{
base.PrepareForSegue(segue, sender);
switch (segue.Identifier)
{
case "UserLoginSegue":
UserLoginController loginSheet = segue.DestinationController as UserLoginController;
var theView = loginSheet.View;
loginSheet.Username = "";
loginSheet.Password = "";
loginSheet.Presentor = this;
loginSheet.DialogAccepted += (object s, EventArgs e) => { Console.WriteLine("OK Clicked"); };
loginSheet.DialogCanceled += (object s, EventArgs e) => { Console.WriteLine("Cancel Clicked"); };
break;
}
}
Related
I didn't know how better to word the title so I went with solution that came to my mind.
Here is the problem. I have a page that has list and each item on the lists opens a detail page (on click). But the VM is reused, which causes me several problems.
Previous data can be seen for split second when opening a the detail page
I need certain properties to be set to specific values when the page open, but since the VM is reused it keeps all the values from the previous detail and this messes up my logic.
This UWP app. I'm using Template10 framework's NavigationService to move between pages.
Main Page ViewModel
public class MainPageViewModel : ViewModelBase {
private List<MangaItem> _mangaList;
public List<MangaItem> mangaList {
get { return _mangaList; }
set { Set(ref _mangaList, value); }
}
private string _mainSearchText;
public string mainSearchText {
get { return _mainSearchText; }
set { Set(ref _mainSearchText, value); }
}
public MainPageViewModel() {
_mangaList = new List<MangaItem>();
mangaList = new List<MangaItem>();
Initialize();
}
private async void Initialize() {
mangaList = await MangaListGet.GetListAsync();
}
public async void MainSearchSubmitted() {
mangaList = await MangaListGet.GetListAsync(_mainSearchText);
}
public void MangaSelected(object sender, ItemClickEventArgs e) {
var mangaItem = (MangaItem)e.ClickedItem;
NavigationService.Navigate(typeof(Views.MangaDetail), mangaItem.id);
}
}
And Detail Page ViewModel
class MangaDetailViewModel : ViewModelBase {
private MangaItem _mangaDetail;
public MangaItem mangaDetail {
get { return _mangaDetail; }
set { Set(ref _mangaDetail, value); }
}
private string _mangaId;
public override async Task OnNavigatedToAsync(object parameter, NavigationMode mode, IDictionary<string, object> suspensionState) {
_mangaId = parameter as string;
Initialize();
await Task.CompletedTask;
}
private async void Initialize() {
mangaDetail = await MangaDetailGet.GetAsync(_mangaId);
}
public void ChapterSelected(object sender, ItemClickEventArgs e) {
var _chapterId = (ChapterListItem)e.ClickedItem;
NavigationService.Navigate(typeof(Views.ChapterPage), _chapterId.id);
}
}
This code only shows the first problem is displaying previously loaded data for a split second. If needed I will add code that showcases the other problem, but I' not sure if it's really relevant right now. I'm thinking that maybe my entire logic is flawed or something.
EDIT:
<Page.DataContext>
<vm:ChapterPageViewModel x:Name="ViewModel" />
</Page.DataContext>
where vm is xmlns:vm="using:MangaReader.ViewModels".
Another solution is to use Bootstrapper.ResolveforPage() which is intended to handle dependency injection but would easily serve your needs. Like this:
[Bindable]
sealed partial class App : BootStrapper
{
static ViewModels.DetailPageViewModel _reusedDetailPageViewModel;
public override INavigable ResolveForPage(Page page, NavigationService navigationService)
{
if (page.GetType() == typeof(Views.DetailPage))
{
if (_reusedDetailPageViewModel == null)
{
_reusedDetailPageViewModel = new ViewModels.DetailPageViewModel();
}
return _reusedDetailPageViewModel;
}
else
{
return null;
}
}
}
The NavigationService will treat this the same as any other view-model. Meaning it will call OnNavTo() and the other navigation overrides you include.
Best of luck.
While Template10 documentation states the NavigationCacheMode is disabled by default, that isn't the case in it's example templates (as of writing this). This is set in View C# code (.xaml.cs file).
.xaml.cs file
namespace MangaReader.Views {
public sealed partial class MangaDetail : Page {
public MangaDetail() {
InitializeComponent();
//NavigationCacheMode = Windows.UI.Xaml.Navigation.NavigationCacheMode.Enabled; //this was set by default
NavigationCacheMode = Windows.UI.Xaml.Navigation.NavigationCacheMode.Disabled;
}
}
}
Now, new ViewModel will be created each time you access a this page.
My problem is I have a variable that I need the window to send back, so I am using out to accomplish this. Here is an example of the constructor for the WPF window.
public CustomYesNo(out bool FormFilled)
{
InitializeComponent();
FormFilled = false;
}
The problem i'm having is I want it so one of the other methods in the class will be able to modify the FormFilled variable that gets sent back to the calling class like below.
private void Button_Yes_Click(object sender, RoutedEventArgs e)
{
FormFilled = true;
Close();
}
Obviously the Button_Yes_Click method does not have access to the FormFilled variable, and I am trying to figure out how I could possible change the value of the FormFilled variable from this method since this variable is only in the constructor's scope. Is what I am trying to do possible using 'out' or do I need to go another route?
Try this pattern
Calling Method:
class foo
{
public void bar()
{
DialogForm myDialogForm = new DialogForm();
myDialogForm.ShowDialog();
if (myDialogForm.DialogResult)
{
//Its true
}
}
}
Form window:
public partial class DialogForm : Window
{
public DialogForm()
{
InitializeComponent();
}
void submitButton_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = true;
}
}
The calling method in WPF is slightly different for checking the result:
bool? result = myDialogForm.ShowDialog();
if (result.HasValue && result.Value)
I have separated two projects in my solution because they each require libraries targeting different CPU.
In one of my project, I just have classes that respond to clicks (let's call it ProjectClick 64 bits libraries), the other one is a sort of UI with an MVVM implementation (ProjectUser 32 bits libraries).
The thing I am searching for is a way to let the ProjectUser know that the click has been performed by the ProjectClick, without the Project Click knowing anything else.
What I have tried so far
I have been scattering the web and books to understand a bit more about C#. From what I understood, to communicate, the best way is to create a Interface. I have been looking at this subject for an answer, and have been trying to implement a third project with an interface between the two.
Ok, here goes the code, (this is a purposely simplified code, I hope it is clear enough)
First the Interface (in a console application)
namespace LinkApplication
{
public interface IEvent
{
bool CompareClick { get; set; }
}
}
Then, the project clicking which is a wpf
namespace ProjectClick
public partial class MainWindow : Window, IEvent
{
public MainWindow()
{
try { InitializeComponent(); }
catch (Exception e)
{
Console.WriteLine(e.InnerException);
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
CompareClick = true;
}
private void Button_Leave(object sender, RoutedEventArgs e)
{
CompareClick = false;
}
}
Finally the UI
namespace ProjectUser
{
public partial class MainWindow : Window, IEvent, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
this.WindowStartupLocation = WindowStartupLocation.CenterScreen; //start the window at the centre of the screen
DataContext = this;
}
public bool CompareClick { get; set; }
public bool ClickCheck
{
get { return CompareClick; }
set
{
if (value != CompareClick)
{
CompareClick = value;
OnPropertyChanged("ClickCheck");
}
}
}
You can see the realted Label here in the Window
<Label Content="{Binding ClickCheck}" HorizontalAlignment="Left" Margin="690,358,0,0" VerticalAlignment="Top"/>
Here, the value always stays at false, and I don't really understand the logic of the changing value. I am still learning, and I have seen several other ideas on the web like a custom EventHandler, but I don't really understand the implementation between two projects not knowing each others. I will be glad if someone could route me towards a possible solution, or a better way to perform.
Edit
I would preferably like to avoid referring the Project Click in the ProjectUser to keep the privileges of different CPU targeting. The other way around is not a problem.
Thank you for your kind answers.
I have been greatly advised and have looked into Inter Process Communication between instances. I have looked into different things but the most satisfying answer of all was on Omegaman's blog (bit thanks to this subject).
So basically, I have tried to avoid localhost information, thinking there would be a more straightforward solution. But since we have not thought of anything better, I think this is what I was looking for.
What I have found
So now, the solution here was to use a WCF service with NamedPipes. By creating a Sender and Receiver actions, the two process ProjectUser and ProjectClick never encounter each other directly. You have instead a pipe controlled by the WCF. You can see more details on the blog on how to communicate, I just adapted (without great change) what he did by changing the passing information.
One thing to note however
The processes cannot both start at the same time, and the receiver must start first to listen to the information coming through. Basically, the sender has to start afterwards.
I created two windows in WPF, and a WCFServiceLibrary. When the button is clicked, there is an incrementation, and it shows the number on the second screen.
A little bit of code
You can see a lot on Omegaman's blog, and I will just post what I have changed.
On the ProjectUser side, supposed to receive, the label is updated as follows
Receiver pipe = new Receiver();
public MainWindow()
{
InitializeComponent();
//this.WindowStartupLocation = WindowStartupLocation.CenterScreen; //start the window at the centre of the screen
DataContext = this;
pipe.Data += new PipeLink.PipeService.DataIsReady(DataBeingRecieved);
if (pipe.ServiceOn() == false)
MessageBox.Show(pipe.error.Message);
label1.Content = "Listening to Pipe: " + pipe.CurrentPipeName + Environment.NewLine;
}
void DataBeingRecieved(int data)
{
Dispatcher.Invoke(new Action(delegate()
{
label1.Content += string.Join(Environment.NewLine, data);
label1.Content += Environment.NewLine;
}));
}
On the ProjectClick side, supposed to send, the button click updates as follows
int i;
public MainWindow()
{
try { InitializeComponent(); }
catch (Exception e)
{
Console.WriteLine(e.InnerException);
}
i = 0;
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
int messages;
i++;
Stopwatch stoop = new Stopwatch();
stoop.Start();
messages = i;
try
{
PipeLink.Sender.SendMessage(messages);
stoop.Stop();
Console.WriteLine(stoop.ElapsedMilliseconds + " ms");
}
catch (Exception u)
{
Console.WriteLine(u);
}
}
The important part of the code, is the creation of the pipe itself, using NetNamedPipeBinding. This is where the whole communication will take place
You can see it in the PipeService code :
public class PipeService : IPipeService
{
public static string URI
= "net.pipe://localhost/Pipe";
// This is when we used the HTTP bindings.
// = "http://localhost:8000/Pipe";
#region IPipeService Members
public void PipeIn(int data)
{
if (DataReady != null)
DataReady(data);
}
public delegate void DataIsReady(int hotData);
public DataIsReady DataReady = null;
#endregion
}
What about the speed?
I was afraid simple data may take longer to arrive than on a simple click. I was mistaken : the first number took longer than the others because of the first connection, so about a second. But after that, for clicking about a 100 times, I had a, average of 10 ms (I know it is not significant data, still I thought it was good to test it a couple of times).
I am pushing everything on the GitHub used with Andreas, for anyone who might be interested.
I still do not know if the code is optimized though. Should you have a better solution, I will happily read it.
As others pointed out your concept of interfaces is wrong still. However i get what you're trying to do.
Try this:
namespace LinkApplication
{
public interface IEventReceiver
{
void Receive<T>(T arg) where T : EventArgs;
}
public class SomeUniqueEvent : EventArgs
{
public bool Clicked { get; set; }
public SomeUniqueEvent(bool clicked)
{
Clicked = clicked;
}
}
public static class EventTunnel
{
private static readonly List<IEventReceiver> _receivers = new List<IEventReceiver>();
public static void Publish<T>(T arg) where T : EventArgs
{
foreach (var receiver in _receivers)
{
receiver.Receive(arg);
}
}
public static void Subscribe(IEventReceiver subscriber)
{
_receivers.Add(subscriber);
}
}
}
namespace ProjectClick
{
public partial class MainWindow : Window
{
public MainWindow()
{
try { InitializeComponent(); }
catch (Exception e)
{
Console.WriteLine(e.InnerException);
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
LinkApplication.EventTunnel.Publish(new LinkApplication.SomeUniqueEvent(true));
}
private void Button_Leave(object sender, RoutedEventArgs e)
{
LinkApplication.EventTunnel.Publish(new LinkApplication.SomeUniqueEvent(false));
}
}
}
namespace ProjectUser
{
public partial class MainWindow : Window, LinkApplication.IEventReceiver, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
this.WindowStartupLocation = WindowStartupLocation.CenterScreen; //start the window at the centre of the screen
DataContext = this;
LinkApplication.EventTunnel.Subscribe(this);
}
public bool CompareClick { get; set; }
public bool ClickCheck
{
get { return CompareClick; }
set
{
if (value != CompareClick)
{
CompareClick = value;
OnPropertyChanged("ClickCheck");
}
}
}
public void Receive<T>(T arg) where T : EventArgs
{
var casted = arg as SomeUniqueEvent;
if (casted != null)
{
ClickCheck = casted.Clicked;
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
Here, you misunderstand what an interface is. Every implementation of an interface is a different one. When you click the button, CompareClick property of ProjectClick project's MainWindow changes value. But that doesn't change the ProjectUser project's MainWindow. They are two completely different objects! The best way that I can think of now, is to make the button public. Alternatively, you can create a method in the MainWindow class of the ProjectClick. Use this method to subscribe to the click event. Something like this:
public void SubscribeToClickEvent (EventHandler handler) {
this.Button.Click += handler //whatever your button is called
}
If you want to encapsulate Button, use the method above. If you don't, then just make it public.
And you ask, how can I access an instance of MainWindow to use the method? The only way I can think of is to make MainWindow a singleton.
I have these objects in my project:
SchedulerList
SchedulerListItem
SchedulerListItemDetails
each one is a win forms control, which are used in forms of my application. The SchedulerList holds SchedulerListItems and each item can have SchedulerListItemDetails.
my code goes as follows:
//creating my initial list form
FrmListTesting f = new FrmListTesting();
f.Show();
The form has only one button that has a hard-coded parameter for testing purposes, as well as a SchedulerList control taht will hold the list items.
When the button is clicked the form does the following:
private void button1_Click(object sender, EventArgs e)
{
var control = this.Controls[1] as SchedulerList;
var path = #"D:\Share\Countries.txt";
var sli = new SchedulerListItem(path);
control.AddItem(sli);
}
my SchedulerListItem constuctor goes as follows:
public SchedulerListItem(string path)
{
InitializeComponent();
this.Name = Path.GetFileNameWithoutExtension(path);
this.SourcePath = path;
this.DestinationPath = GetDestinationPath(path);
}
And the AddItem method is defined as:
public void AddItem(SchedulerListItem item)
{
this.flPanel.Controls.Add(item);
}
The add item method works as intended, displays all the data that was required and displays it in the UI. The list item has a button that brings up the details form as such:
//the form constructor
public FrmSchedulerItemDetails(SchedulerListItem item)
{
InitializeComponent();
this.detailsControl = new SchedulerListItemDetails(item, this);
}
//control constructor
public SchedulerListItemDetails(SchedulerListItem item, Form owner)
{
InitializeComponent();
this.SourcePath = item.SourcePath;
this.DestinationPath = item.DestinationPath;
this.OldFormat = item.OldFormat;
this.ExportToExcel = item.ExportToExcel;
this.owner = owner;
this.underlyingItem = item;
}
And now the problem. After the SchedulerListItemDetails constructor is called and the data "gets initialized", when i look at the data inside the object its set to default values. it seams that everything that I set after InitializeComponent(); gets ignored.
things that i have tried:
hard-coding the values to see if primitives get passed correctly
settings breakpoints on every InitializeComponent() method to see the stack trace associated with setting to default values
none of the methods show any results... I know that if i use a form directly instead of using a control within a from i can set the values the way i want to, but I'm very confused as to why this other method with controls doesn't work.
EDIT 1:
the code for SchedulerListItemDetails:
public partial class SchedulerListItemDetails : UserControl
{
public SchedulerListItemDetails(SchedulerListItem item, Form owner)
{
InitializeComponent();
this.SourcePath = item.SourcePath;
this.DestinationPath = item.DestinationPath;
this.OldFormat = item.OldFormat;
this.ExportToExcel = item.ExportToExcel;
this.owner = owner;
this.underlyingItem = item;
}
public SchedulerListItemDetails()
{
InitializeComponent();
}
private Form owner = null;
private SchedulerListItem underlyingItem;
public Boolean ExportToExcel
{
get
{
return this.cbxExcel.Checked;
}
set
{
this.cbxExcel.Checked = value;
}
}
public Boolean OldFormat
{
get
{
return this.cbxOldFormat.Checked;
}
set
{
this.cbxOldFormat.Checked = value;
}
}
public String DestinationPath
{
get
{
return this.tbxDestinationPath.Text;
}
set
{
this.tbxDestinationPath.Text = value;
}
}
public String SourcePath
{
get
{
return this.tbxSourcePath.Text;
}
set
{
this.tbxSourcePath.Text = value;
}
}
private void btnCancel_Click(object sender, EventArgs e)
{
this.owner.Close();
}
private void btnSave_Click(object sender, EventArgs e)
{
underlyingItem.SourcePath = this.SourcePath;
underlyingItem.DestinationPath = this.DestinationPath;
underlyingItem.OldFormat = this.OldFormat;
underlyingItem.ExportToExcel = this.ExportToExcel;
btnCancel_Click(sender, e);
}
}
I'll make an answer, because it should help you to solve your problem.
You have default (parameterless) constructor, which may be called and if it is called, then your constructor with parameters is not called.
Proper design would be something like
public partial class SchedulerListItemDetails : UserControl
{
public SchedulerListItemDetails()
{
InitializeComponent();
}
public SchedulerListItemDetails(SchedulerListItem item, Form owner): this()
{
this.SourcePath = item.SourcePath;
...
}
}
Notice this(), this ensure what parameterless constructor is called before (and InitializeComponent() as well, no need to duplicate it in another constructor).
Back to your problem. In your case it's like this
public partial class SchedulerListItemDetails : UserControl
{
public SchedulerListItemDetails()
{
InitializeComponent();
}
public SchedulerListItemDetails(SchedulerListItem item, Form owner)
{
InitializeComponent();
this.SourcePath = item.SourcePath;
...
}
}
Only one constructor can be called. So if you put breakpoint in parameterless one and it's triggered, then you have problems. Because you create somewhere SchedulerListItemDetails without setting it's properties (they stay default).
More likely problem is that you create new instance of that object (either before or after constructing proper, if your code ever construct such object) and that instance is what you inspect later.
So after i got a quick course of how win forms work i figured out what the problem was.
my code that i thought was enough is:
public FrmSchedulerItemDetails(SchedulerListItem item)
{
InitializeComponent();
this.DetailsControl = new SchedulerListItemDetails(item, this);
}
public SchedulerListItemDetails DetailsControl
{
get
{
return this.detailsControl;
}
set
{
this.detailsControl = value;
}
}
the this.detailsControl is the control im trying to setup, but as i have learned the correct way of replacing a component for a new one is:
public FrmSchedulerItemDetails(SchedulerListItem item)
{
InitializeComponent();
this.DetailsControl = new SchedulerListItemDetails(item, this);
}
public SchedulerListItemDetails DetailsControl
{
get
{
return this.detailsControl;
}
set
{
this.Controls.Remove(this.detailsControl);
this.detailsControl = value;
this.Controls.Add(this.detailsControl);
}
}
Feel kinda silly now :).
To simply illustrate my dilemma, let say that I have the following code:
class A
{
// May be set by a code or by an user.
public string Property
{
set { PropertyChanged(this, EventArgs.Empty); }
}
public EventHandler PropertyChanged;
}
class B
{
private A _a;
public B(A a)
{
_a = a;
_a.PropertyChanged += Handler;
}
void Handler(object s, EventArgs e)
{
// Who changed the Property?
}
public void MakeProblem()
{
_a.Property = "make a problem";
}
}
In order to perform its duty, class B have to react on A's PropertyChanged event but also is capable of alternating that property by itself in certain circumstances. Unfortunately, also other objects can interact with the Property.
I need a solution for a sequential flow. Maybe I could just use a variable in order to disable an action:
bool _dontDoThis;
void Handler(object s, EventArgs e)
{
if (_dontDoThis)
return;
// Do this!
}
public void MakeProblem()
{
_dontDoThis = true;
_a.Property = "make a problem";
_dontDoThis = false;
}
Are there a better approaches?
Additional considerations
We are unable to change A.
A is sealed.
There are also other parties connected to the PropertyChanged event and I don't know who their are. But when I update the Property from B, they shouldn't be also notified. But I'm unable to disconnect them from the event because I don't know them.
What if also more threads can interact with the Property in the mean time?
The more bullets solved, the better.
Original problem
My original problem is a TextBox (WPF) that I want to complement depending on its content and focus. So I need to react on TextChanged event and I also need to omit that event if its origin is derived from my complements. In some cases, other listeners of a TextChanged event shouldn't be notified. Some strings in certain state and style are invisible to others.
If it is so important not to handle events you initiated, maybe you should change the way you set Property to include the initiator of the change?
public class MyEventArgs : EventArgs
{
public object Changer;
}
public void SetProperty(string p_newValue, object p_changer)
{
MyEventArgs eventArgs = new MyEventArgs { Changer = p_changer };
PropertyChanged(this, eventArgs);
}
And then in your handler - simply check your are not the initiator.
I find all these changes in registration and members very problematic in terms on multi threading and extensibility.
Well essentially you are trying to break the event delegation mechanism and any "solution" to that is going to be brittle since updates to the BCL might break your code. You could set the backing field using reflection. This of course would require that you do have permissions to do this and seeing the generic framing of the question it might not always be that you have the needed permissions
public void MakeProblem()
{
if (_backingField == null) {
_backingField = = _a.GetType().GetField(fieldname)
}
_backingField.SetValue(_a,"make a problem");
}
but as I started out, you are trying to break the event delegation mechanism. The idea is that the receivers of the event are independent. Disabling might lead to so very hard to find bugs because looking at any given piece of code it looks correct but only when you realize that some devious developer has hack the delegation mechanism do you realize why the information that is shown on screen seems to be a cached version of the actual value. The debugger shows the expected value of the property but because the event was hidden the handler responsible for updating the display was never fired and hence an old version is displayed (or the log shows incorrect information so when you are trying to recreate a problem a user has reported based on the content of the log you will not be able to because the information in the log is incorrect because it was based on no one hacking the event delegation mechanism
To my opinion your solution is possible, though I would have created a nested IDisposable class inside B that does the same thing with 'using', or put the '_dontDoThis = false' inside a 'finally' clause.
class A
{
// May be set by a code or by an user.
public string Property
{
set { if (!_dontDoThis) PropertyChanged(this, EventArgs.Empty); }
}
public EventHandler PropertyChanged;
bool _dontDoThis;
}
class B
{
private class ACallWrapper : IDisposable
{
private B _parent;
public ACallWrapper(B parent)
{
_parent = parent;
_parent._a._dontDoThis = true;
}
public void Dispose()
{
_parent._a._dontDoThis = false;
}
}
private A _a;
public B(A a)
{
_a = a;
_a.PropertyChanged += Handler;
}
void Handler(object s, EventArgs e)
{
// Who changed the Property?
}
public void MakeProblem()
{
using (new ACallWrapper(this))
_a.Property = "make a problem";
}
}
On the other hand, I would've used the 'internal' modifier for these things if those two classes are inside the same assembly.
internal bool _dontDoThis;
That way, you keep a better OOP design.
Moreover, if both classes are on the same assembly, I would've written the following code inside A:
// May be set by a code or by an user.
public string Property
{
set
{
internalSetProperty(value);
PropertyChanged(this, EventArgs.Empty);
}
}
internal internalSetProperty(string value)
{
// Code of set.
}
In this case, B could access internalSetProperty without triggering to PropertyChanged event.
Thread Sync:
NOTE: The next section applies to WinForms - I don't know if it applies to WPF as well.
For thread synchronizations, because we're referring to a control. you could use the GUI thread mechanism for synchronization:
class A : Control
{
public string Property
{
set
{
if (this.InvokeRequired)
{
this.Invoke((Action<string>)setProperty, value);
reutrn;
}
setProperty(value);
}
}
private void setProperty string()
{
PropertyChanged(this, EventArgs.Empty);
}
}
Great question.
As a general case, you can not mess around with event handlers of sealed classes. Normally you could override A's hypothetical OnPropertyChanged and based on some flag either raise the event or not. Alternatively you could provide a setter method that does not raise event, as suggested by #Vadim. However, if A is sealed your best option is to add flag to a lister, just as you did. That will enable you to recognize PropertyChanged event raised by B, but you won't be able to suppress the event for other listeners.
Now, since you provided context... There is a way of doing exactly this in WPF. All that needs to be done is B's handler for TextBox.TextChanged needs to set e.Handled = _dontDoThis. That will supress notifications for all other listeners, provided B's one was added as the first one. How to make sure this happens? Reflection!
UIElement exposes only AddHandler and RemoveHandler methods, there is no InsertHandler that would allow to manually specifiy the priority for the handler. However, a quick peek into .NET source code (either download the whole thing or query what you need) reveals that AddHandler forwards arguments to an interal method EventHandlersStore.AddRoutedEventHandler, which does this:
// Create a new RoutedEventHandler
RoutedEventHandlerInfo routedEventHandlerInfo =
new RoutedEventHandlerInfo(handler, handledEventsToo);
// Get the entry corresponding to the given RoutedEvent
FrugalObjectList<RoutedEventHandlerInfo> handlers = (FrugalObjectList<RoutedEventHandlerInfo>)this[routedEvent];
if (handlers == null)
{
_entries[routedEvent.GlobalIndex] = handlers = new FrugalObjectList<RoutedEventHandlerInfo>(1);
}
// Add the RoutedEventHandlerInfo to the list
handlers.Add(routedEventHandlerInfo);
All this stuff is internal, but can be recreated using reflection:
public static class UIElementExtensions
{
public static void InsertEventHandler(this UIElement element, int index, RoutedEvent routedEvent, Delegate handler)
{
// get EventHandlerStore
var prop = typeof(UIElement).GetProperty("EventHandlersStore", BindingFlags.NonPublic | BindingFlags.Instance);
var eventHandlerStoreType = prop.PropertyType;
var eventHandlerStore = prop.GetValue(element, new object[0]);
// get indexing operator
PropertyInfo indexingProperty = eventHandlerStoreType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance)
.Single(x => x.Name == "Item" && x.GetIndexParameters().Length == 1 && x.GetIndexParameters()[0].ParameterType == typeof(RoutedEvent));
object handlers = indexingProperty.GetValue(eventHandlerStore, new object[] { routedEvent });
if (handlers == null)
{
// just add the handler as there are none at the moment so it is going to be the first one
if (index != 0)
{
throw new ArgumentOutOfRangeException("index");
}
element.AddHandler(routedEvent, handler);
}
else
{
// create routed event handler info
var constructor = typeof(RoutedEventHandlerInfo).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).Single();
var handlerInfo = constructor.Invoke(new object[] { handler, false });
var insertMethod = handlers.GetType().GetMethod("Insert");
insertMethod.Invoke(handlers, new object[] { index, handlerInfo });
}
}
}
Now calling InsertEventHandler(0, textBox, TextBox.TextChangedEvent, new TextChangedEventHandler(textBox_TextChanged)) will make sure your handler will be the first one on the list, enabling you to suppress notifications for other listeners!
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var textBox = new TextBox();
textBox.TextChanged += (o, e) => Console.WriteLine("External handler");
var b = new B(textBox);
textBox.Text = "foo";
b.MakeProblem();
}
}
class B
{
private TextBox _a;
bool _dontDoThis;
public B(TextBox a)
{
_a = a;
a.InsertEventHandler(0, TextBox.TextChangedEvent, new TextChangedEventHandler(Handler));
}
void Handler(object sender, TextChangedEventArgs e)
{
Console.WriteLine("B.Handler");
e.Handled = _dontDoThis;
if (_dontDoThis)
{
e.Handled = true;
return;
}
// do this!
}
public void MakeProblem()
{
try
{
_dontDoThis = true;
_a.Text = "make a problem";
}
finally
{
_dontDoThis = false;
}
}
}
Output:
B.Handler
External handler
B.Handler
I found one solution with regard to third parties, that are connected to the property and we don't want to nofify them when that property changed.
There are though the requirements:
We are capable of override the A.
The A has a virtual method that is invoked when property changed and allows to suspend the event to be raised.
The event is raised immediately when property is being changed.
The solution is to replace the A by MyA, as follows:
class A
{
// May be set by a code or by an user.
public string Property
{
set { OnPropertyChanged(EventArgs.Empty); }
}
// This is required
protected virtual void OnPropertyChanged(EventArgs e)
{
PropertyChanged(this, e);
}
public EventHandler PropertyChanged;
}
// Inject MyA instead of A
class MyA : A
{
private bool _dontDoThis;
public string MyProperty
{
set
{
try
{
_dontDoThis = true;
Property = value;
}
finally
{
_dontDoThis = false;
}
}
}
protected override void OnPropertyChanged(EventArgs e)
{
// Also third parties will be not notified
if (_dontDoThis)
return;
base.OnPropertyChanged(e);
}
}
class B
{
private MyA _a;
public B(MyA a)
{
_a = a;
_a.PropertyChanged += Handler;
}
void Handler(object s, EventArgs e)
{
// Now we know, that the event is not raised by us.
}
public void MakeProblem()
{
_a.MyProperty = "no problem";
}
}
Unfortunately we still use back bool field and we assume a single thread. To rid of the first, we could use a refactored solution suggest by EZSlaver (here). First, create a disposable wrapper:
class Scope
{
public bool IsLocked { get; set; }
public static implicit operator bool(Scope scope)
{
return scope.IsLocked;
}
}
class ScopeGuard : IDisposable
{
private Scope _scope;
public ScopeGuard(Scope scope)
{
_scope = scope;
_scope.IsLocked = true;
}
public void Dispose()
{
_scope.IsLocked = false;
}
}
Then the MyProperty might be refactored to:
private readonly Scope _dontDoThisScope = new Scope();
public string MyProperty
{
set
{
using (new ScopeGuard (_dontDoThisScope))
Property = value;
}
}