I've been doing a refactor of an existing application, and I am attempting to use an Attribute on a property to trigger the NotifyPropertyChanged event using Unity interception. I got to the point where the event was firing, but the controls were not updating.
I wasn't sure if it was invoking the event correctly, so on my ViewModelBase I created a DispatchPropertyChanged method that invokes the property changed event. This method works to launch property changed when called directly from a view model, but when called from a ViewModel retrieved through reflection inside the intercept handler, it doesn't work.
I've inserted a link to the https://www.dropbox.com/s/9qg2n0gd2n62elc/WPFUnityTest.zip. If you open this solution and run the application, then click the "Reset" button, you'll see that the "Normal" text box updates, but the "Unity" text box does not update.
If you place a breakpoint at line 65 of the MainWindowViewModel and line 53 of the NotifyPropertyChangedHandler you'll see that the handler is working, the dispatch method is being called, and the event is being invoked. However, only the "Normal" one updates.
Any help on why the "Unity" text box isn't updating would be wonderful, thanks!
Amanda
EDIT:
Sorry for not including this originally, I really had no idea where the problem was. This was the original code for the interception behavior that was correct below:
public class NotifyPropertyChangedHandler : IInterceptionBehavior
{
public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
{
var npcAttribute = getNotifyPropertyChangedAttributeForSetter(input) as NotifyPropertyChangedAttribute;
if (npcAttribute != null)
{
if (npcAttribute.TimingOption == PropertyChangedTiming.Always||
shouldRaiseEvent(input))
{
raiseEvent(input);
}
}
return getNext()(input, getNext);
}
public IEnumerable<Type> GetRequiredInterfaces()
{
return new[] { typeof(INotifyPropertyChanged) };
}
public bool WillExecute { get { return true; } }
private object getNotifyPropertyChangedAttributeForSetter(IMethodInvocation input)
{
if (!input.MethodBase.Name.StartsWith("set_"))
{
return null;
}
return input.MethodBase.ReflectedType.GetProperty(input.MethodBase.Name.Substring(4))
.GetCustomAttributes(true).SingleOrDefault(attr => attr.GetType() == typeof (NotifyPropertyChangedAttribute));
}
private void raiseEvent(IMethodInvocation input)
{
raiseEvent(input, new PropertyChangedEventArgs(input.MethodBase.Name.Substring(4)));
}
private void raiseEvent(IMethodInvocation input, PropertyChangedEventArgs eventArgs)
{
var viewModel = input.Target as ViewModelBase;
if (viewModel != null)
{
viewModel.DispatchPropertyChangedEvent(eventArgs.PropertyName);
}
}
private static bool shouldRaiseEvent(IMethodInvocation input)
{
var methodBase = input.MethodBase;
if (!methodBase.IsSpecialName || !methodBase.Name.StartsWith("set_"))
{
return false;
}
var propertyName = methodBase.Name.Substring(4);
var property = methodBase.ReflectedType.GetProperty(propertyName);
var getMethod = property.GetGetMethod();
if (getMethod == null)
{
return false;
}
var oldValue = getMethod.Invoke(input.Target, null);
var value = input.Arguments[0];
return (value == null) ? oldValue !=null :
!value.Equals(oldValue);
}
}
There is a problem in your NotifyPropertyChangedHandler.Invoke method code:
Your Code
I have added some comments to describe what the problem is with this code.
public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
{
var npcAttribute = getNotifyPropertyChangedAttributeForSetter(input) as NotifyPropertyChangedAttribute;
if (npcAttribute != null)
{
if (npcAttribute.TimingOption == PropertyChangedTiming.Always||
shouldRaiseEvent(input))
{
// You are raising property changed event here,
// however the value of the property is not changed until getNext()(input, getNext) called
// So, WPF will re-read the same "old" value and you don't see anything updated on the screen
raiseEvent(input);
}
}
// Property value is updated here!!!
return getNext()(input, getNext);
}
Change that code to something like:
public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
{
var temp = false;
var npcAttribute = getNotifyPropertyChangedAttributeForSetter(input) as NotifyPropertyChangedAttribute;
if (npcAttribute != null)
{
if (npcAttribute.TimingOption == PropertyChangedTiming.Always||
shouldRaiseEvent(input))
{
temp = true;
}
}
var returnValue = getNext()(input, getNext); // Changing the value here
if (temp) raiseEvent(input); // Raising property changed event, if necessary
return returnValue;
}
I have tested this code and it works. Hope this helps!
Related
I am trying to achieve perfect MVVM in Xamarin.Forms with Entry.
My model contains properties whose type includes string, int?, decimal?, bool?, etc,. Whenever I bind to a string type, the two way binding works because the text property has a string type (they match). But once you try to bind back to the model and the property is an int or int?, it doesn't update the model's property's value.
During my research and with help from Xamarin support, this was a very helpful thread on how to handle nullable types:
Nullable type in x:TypeArguments
XAML code:
<controls:NullableIntEntry Grid.Column="1" Grid.Row="14" NumericText="{Binding BusinessOwnership, Mode=TwoWay}" x:Name="lblBusinessOwnership"></controls:NullableIntEntry>
BindableEntry (Entry extension) code:
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Reflection;
using Xamarin.Forms;
namespace CreditBuilderApp.Controls
{
public class BindableEntry<T> : Entry
{
static bool firstLoad;
public static readonly BindableProperty NumericTextProperty =
BindableProperty.Create("NumericText", typeof(T), typeof(BindableEntry<T>),
null, BindingMode.TwoWay, propertyChanged: OnNumericTextChanged);
static void OnNumericTextChanged(BindableObject bindable, object oldValue, object newValue)
{
var boundEntry = (BindableEntry<T>)bindable;
if (firstLoad && newValue != null)
{
firstLoad = false;
boundEntry.Text = newValue.ToString();
}
}
public T NumericText
{
get { return (T)GetValue(NumericTextProperty); }
set { SetValue(NumericTextProperty, value); }
}
public BindableEntry()
{
firstLoad = true;
this.TextChanged += BindableEntry_TextChanged;
}
private void BindableEntry_TextChanged(object sender, TextChangedEventArgs e)
{
if (!String.IsNullOrEmpty(e.NewTextValue))
{
this.NumericText = (T)Convert.ChangeType(e.NewTextValue, typeof(T));
}
else
{
this.NumericText = default(T);
}
}
}
}
NullableIntEntry and NullableDecimalEntry (Bindable Entry extension specifying the type):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CreditBuilderApp.Controls
{
public class NullableIntEntry : BindableEntry<Int32?>
{
}
public class NullableDecimalEntry : BindableEntry<Decimal?>
{
}
}
Model:
private int? _businessOwnership { get; set; }
public int? BusinessOwnership
{
get { return _businessOwnership; }
set
{
if (_businessOwnership != value)
{
_businessOwnership = value;
RaisePropertyChanged();
}
}
}
I am actually able to bind to integer, decimal, floats, basically any type that isn't a string, which is a step in the right direction. BUT, to accomplish this, I have to create the BindableEntry above and specify which type it is. (Replace T with int?, T with decimal?, etc,. ALONG with specifying how the e.NewTextValue is casted.
PROBLEM: the below type change conversion is breaking the two way binding.
this.NumericText = (T)Convert.ChangeType(e.NewTextValue, typeof(T));
But, that gives me an error (obviously) since this.NumericText is of type T before run time.
So, if I wanted the entry to work for nullable integers, I would need to replace all type T's with int? AS WELL AS change the above code to:
Convert.ToInt32(e.NewTextValue)
When I step through the code, whenever I reach the Convert.ChangeType to T line, it exits the frame. There is no error and the page is displayed, but every control after that particular bindable entry, does not have values.
After stepping through the ConvertType function
Let me know if I missed any information. Help would be appreciated!
I recommend using value converter, it's lot easier than creating custom controls and confirms to the MVVM pattern.
You can find the code snippet here - https://forums.xamarin.com/discussion/comment/60076/#Comment_60076
I know this is over a year old but found a solution to this question that may help others. If you make the following change to the original code presented it works:
BindableEntry (Entry extension) code:
Change BindableEntry_TextChanged(object sender, TextChangedEventArgs e) to
private void BindableEntry_TextChanged(object sender, TextChangedEventArgs e)
{
if (!string.IsNullOrEmpty(e.NewTextValue))
{
var t = typeof(T);
t = Nullable.GetUnderlyingType(t);
if (typeof(T) == typeof(decimal?))
{
var results = decimal.TryParse(e.NewTextValue, out var tmpValue) ?
tmpValue : (decimal?) null;
if (results != null)
NumericText = (T)Convert.ChangeType(results, t);
else
NumericText = default(T);
}
else if (typeof(T) == typeof(double?))
{
var results = double.TryParse(e.NewTextValue, out var tmpValue) ?
tmpValue : (double?)null;
if (results != null)
NumericText = (T)Convert.ChangeType(results, t);
else
NumericText = default(T);
}
else if (typeof(T) == typeof(int?))
{
var results = int.TryParse(e.NewTextValue, out var tmpValue) ?
tmpValue : (int?)null;
if (results != null)
NumericText = (T)Convert.ChangeType(results, t);
else
NumericText = default(T);
}
}
else
{
NumericText = default(T);
}
}
I found the missing piece to get this working here.
Hope this helps...
Here I have a functional CloseTabAction using Prism 6 Toolkit found below:
class CloseTabAction : TriggerAction<Button>
{
protected override void Invoke(object parameter)
{
var args = parameter as RoutedEventArgs;
if (args == null) return;
var tabItem = FindParent<TabItem>(args.OriginalSource as DependencyObject);
if (tabItem == null) return;
var tabControl = FindParent<TabControl>(tabItem);
if (tabControl == null) return;
var region = RegionManager.GetObservableRegion(tabControl).Value;
if (region == null) return;
if (region.Views.Contains(tabItem.Content))
{
region.Remove(tabItem.Content);
}
}
static T FindParent<T>(DependencyObject child) where T : DependencyObject
{
while (true)
{
var parentObject = VisualTreeHelper.GetParent(child);
if (parentObject == null) return null;
var parent = parentObject as T;
if (parent != null) return parent;
child = parentObject;
}
}
}
My problem is I want to close the TabItem only if the user selects 'Yes' (Saves to database and closes) or 'No' (Does not save but closes the tab).
I've created a custom popup window to do this however I'm not sure how to wire it up to the CloseTabAction. How would it know if the conformation is true or false?
This is a rough idea:
if (region.Views.Contains(tabItem.Content) && Status)
{
region.Remove(tabItem.Content);
}
public bool Status { get; set; }
private void RaiseClosePopup()
{
ClosePopupRequest.Raise(new Confirmation{Title = "Confrim", Content = "Are you sure you want to close this view?"}, r => Status = r.Confirmed);
}
I see you watched my Pluralsight course on mastering the TabControl with Prism :)
Why don't you just show a dialog before you call "Remove". If the result of the dialog is valid, then continue to remove, if not then don't remove. Then you just need to decide how you will notify your ViewModel of the action. This can be done a number of ways; event aggregator, expose a property on your CloseTabAction trigger, expose a command on your trigger. You can decide how you want to do that.
EDIT: I actually though of a better way. Just use an interface off of your ViewModel that you check to see if you can close a TabItem. Maybe use the IConfirmNavigationRequest interface or create your own. Then check that before removing he region. That's even easier.
In the invoke method I added this block of code. It checks if it implements my custom 'IConfrimCloseRequest' Interface, then calls the 'ConfrimCloseRequest' Method based from that result close or cancel. If it doesn't implement the interface just close it anyway.
var context = new NavigationContext(region.NavigationService, null);
if (IfImplements<IConfirmCloseRequest>(tabItem.Content,
i => i.ConfirmCloseRequest(context, canClose =>
{
if (!canClose) return;
if (region.Views.Contains(tabItem.Content))
region.Remove(tabItem.Content);
}))) return;
if (region.Views.Contains(tabItem.Content))
region.Remove(tabItem.Content);
private static T Implementor<T>(object content) where T : class
{
T implementor = content as T;
if (implementor != null) return implementor;
var element = content as FrameworkElement;
if (element != null) implementor = element.DataContext as T;
return implementor;
}
private static bool IfImplements<T>(object content, Action<T> action) where T : class
{
T target = Implementor<T>(content);
if (target == null) return false;
action(target);
return true;
}
Here In my ViewModel I implement my custom Interface with the method 'ConfirmCloseRequest' which will be called when closing the tab.
public void ConfirmCloseRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
ClosePopupRequest.Raise(new Confirmation { Title = "Confrim", Content = "Are you sure you want to close this view?" }, r => { continuationCallback(r.Confirmed); });
}
Currently I'm implementing a Screen indicating wheater a module is not existing or still in development.
The Back Button has the following code:
regionNavigationService.Journal.GoBack();
This is working as expected. But the user is not coming from the Home Screen. So I need to access the View Name from the last Entry in Navigation Journal.
Example: User is coming from Settings Screen => The text should display "Back to Settings Screen"
Assuming the view name you are looking for is when you do new Uri("Main", UriKind.Relative) that you would want the word Main as the view name.
The forward and backward stacks in the RegionNavigationJournal are private. You could use reflection to get access to it.
var journal = regionNavigationService.Journal as RegionNavigationJournal;
if (journal != null)
{
var stack =
(Stack<IRegionNavigationJournalEntry>)
typeof (RegionNavigationJournal).GetField("backStack",
BindingFlags.NonPublic | BindingFlags.Instance)
.GetValue(journal);
var name = stack.Peek().Uri.OriginalString;
}
Or a better way is to implement your own IRegionNavigationJournal that is a wrapper around it. This is using Unity to constructor inject the default RegionNavigationJournal if using MEF you might need to put the ImportingConstructorAttribute on it.
public class RegionNavigationJournalWrapper : IRegionNavigationJournal
{
private readonly IRegionNavigationJournal _regionNavigationJournal;
private readonly Stack<Uri> _backStack = new Stack<Uri>();
// Constructor inject prism default RegionNavigationJournal to wrap
public RegionNavigationJournalWrapper(RegionNavigationJournal regionNavigationJournal)
{
_regionNavigationJournal = regionNavigationJournal;
}
public string PreviousViewName
{
get
{
if (_backStack.Count > 0)
{
return _backStack.Peek().OriginalString;
}
return String.Empty;
}
}
public bool CanGoBack
{
get { return _regionNavigationJournal.CanGoBack; }
}
public bool CanGoForward
{
get { return _regionNavigationJournal.CanGoForward; }
}
public void Clear()
{
_backStack.Clear();
_regionNavigationJournal.Clear();
}
public IRegionNavigationJournalEntry CurrentEntry
{
get { return _regionNavigationJournal.CurrentEntry; }
}
public void GoBack()
{
// Save current entry
var currentEntry = CurrentEntry;
// try and go back
_regionNavigationJournal.GoBack();
// if currententry isn't equal to previous entry then we moved back
if (CurrentEntry != currentEntry)
{
_backStack.Pop();
}
}
public void GoForward()
{
// Save current entry
var currentEntry = CurrentEntry;
// try and go forward
_regionNavigationJournal.GoForward();
// if currententry isn't equal to previous entry then we moved forward
if (currentEntry != null && CurrentEntry != currentEntry)
{
_backStack.Push(currentEntry.Uri);
}
}
public INavigateAsync NavigationTarget
{
get { return _regionNavigationJournal.NavigationTarget; }
set { _regionNavigationJournal.NavigationTarget = value; }
}
public void RecordNavigation(IRegionNavigationJournalEntry entry)
{
var currentEntry = CurrentEntry;
_regionNavigationJournal.RecordNavigation(entry);
// if currententry isn't equal to previous entry then we moved forward
if (currentEntry != null && CurrentEntry == entry)
{
_backStack.Push(currentEntry.Uri);
}
}
}
If using unity in your Prism Bootstrapper you will need to replace the default registration of the IRegionNavigationJournal
protected override void ConfigureContainer()
{
this.RegisterTypeIfMissing(typeof(IRegionNavigationJournal), typeof(RegionNavigationJournalWrapper), false);
base.ConfigureContainer();
}
If using MEF you will need to put the ExportAttribute on top of the RegionNavigationJournalWrapper
[Export(typeof(IRegionNavigationJournal))]
You can see http://msdn.microsoft.com/en-us/library/gg430866%28v=pandp.40%29.aspx for more information on replacing their default implementation with your own. Once you have the wrapper you will still need to cast it as RegionNavigationJournalWrapper to get access to the PreviousViewName so still not perfect or create an interface that RegionNavigationJournalWrapper also implements to cast to that to get you access to the PreviousViewName
Say I have a BindingList<Person> where Person has a public string property called Name. Is there a way to effectively (if not directly) bind to the Current property (which is a Person object) and then index into the Name property?
I'm imagining a binding set up like
nameLabel.DataBindings.Add(
new Binding("Text", this.myBindingListSource, "Current.Name", true));
or
nameLabel.DataBindings.Add(
new Binding("Text", this.myBindingListSource.Current, "Name", true));
Both of these approaches produce runtime errors.
Currently, I am simply subscribing to the BindingList's CurrentChanged event and handling updates in there. This works but I would prefer the DataBinding approach if possible.
Using the NestedBindingProxy class provided below, you can do
nameLabel.DataBindings.Add(
new Binding("Text", new NestedBindingProxy(this.myBindingListSource, "Current.Name"), true));
Below is the c# code for NestedBindingProxy. The problem with WinForms data binding is it doesn't detect value changes when you use a navigation path that contains multiple properties. WPF does this though. So I created the class NestedBindingProxy that does the change detection and it exposes a property called "Value" that the windows binding can bind too. Whenever any property changes in the navigation path, the notify property changed event will fire for the "Value" property.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
namespace WindowsFormsApplication4
{
public sealed class NestedBindingProxy : INotifyPropertyChanged
{
class PropertyChangeListener
{
private readonly PropertyDescriptor _prop;
private readonly WeakReference _prevOb = new WeakReference(null);
public event EventHandler ValueChanged;
public PropertyChangeListener(PropertyDescriptor property)
{
_prop = property;
}
public object GetValue(object obj)
{
return _prop.GetValue(obj);
}
public void SubscribeToValueChange(object obj)
{
if (_prop.SupportsChangeEvents)
{
_prop.AddValueChanged(obj, ValueChanged);
_prevOb.Target = obj;
}
}
public void UnsubsctribeToValueChange()
{
var prevObj = _prevOb.Target;
if (prevObj != null)
{
_prop.RemoveValueChanged(prevObj, ValueChanged);
_prevOb.Target = null;
}
}
}
private readonly object _source;
private PropertyChangedEventHandler _subscribers;
private readonly List<PropertyChangeListener> _properties = new List<PropertyChangeListener>();
private readonly SynchronizationContext _synchContext;
public event PropertyChangedEventHandler PropertyChanged
{
add
{
bool hadSubscribers = _subscribers != null;
_subscribers += value;
bool hasSubscribers = _subscribers != null;
if (!hadSubscribers && hasSubscribers)
{
ListenToPropertyChanges(true);
}
}
remove
{
bool hadSubscribers = _subscribers != null;
_subscribers -= value;
bool hasSubscribers = _subscribers != null;
if (hadSubscribers && !hasSubscribers)
{
ListenToPropertyChanges(false);
}
}
}
public NestedBindingProxy(object source, string nestedPropertyPath)
{
_synchContext = SynchronizationContext.Current;
_source = source;
var propNames = nestedPropertyPath.Split('.');
Type type = source.GetType();
foreach (var propName in propNames)
{
var prop = TypeDescriptor.GetProperties(type)[propName];
var propChangeListener = new PropertyChangeListener(prop);
_properties.Add(propChangeListener);
propChangeListener.ValueChanged += (sender, e) => OnNestedPropertyChanged(propChangeListener);
type = prop.PropertyType;
}
}
public object Value
{
get
{
object value = _source;
foreach (var prop in _properties)
{
value = prop.GetValue(value);
if (value == null)
{
return null;
}
}
return value;
}
}
private void ListenToPropertyChanges(bool subscribe)
{
if (subscribe)
{
object value = _source;
foreach (var prop in _properties)
{
prop.SubscribeToValueChange(value);
value = prop.GetValue(value);
if (value == null)
{
return;
}
}
}
else
{
foreach (var prop in _properties)
{
prop.UnsubsctribeToValueChange();
}
}
}
private void OnNestedPropertyChanged(PropertyChangeListener changedProperty)
{
ListenToPropertyChanges(false);
ListenToPropertyChanges(true);
var subscribers = _subscribers;
if (subscribers != null)
{
if (_synchContext != SynchronizationContext.Current)
{
_synchContext.Post(delegate { subscribers(this, new PropertyChangedEventArgs("Value")); }, null);
}
else
{
subscribers(this, new PropertyChangedEventArgs("Value"));
}
}
}
}
}
Try this:
Binding bind = new Binding("Text", myBindingListSource, "Current");
bind.Format += (s,e) => {
e.Value = e.Value == null ? "" : ((Person)e.Value).Name;
};
nameLabel.DataBindings.Add(bind);
I haven't tested it but it should work and I've been waiting for the feedback from you.
I stumbled across this by accident (four years after the original post), and after a quick read, I find that the accepted answer appears to be really over-engineered in respect to the OP's question.
I use this approach, and have posted an answer here to (hopefully) help anyone else who has the same problem.
The following should work (if infact this.myBindingListSource implements IBindingList)
I would just create a binding source to wrap my binding list (in my Form/View)
BindingSource bindingSource = new BindingSource(this.myBindingListSource, string.Empty);
And then just bind to the binding source like this:
nameLabel.DataBindings.Add(new Binding("Text", bindingSource, "Name"));
I think the OP's original code didn't work because there is no member called 'Current' on a BindingList (unless the OP has some sort of specialised type not mentioned).
I have written my own Validator and although the validator appears to be working (as it does display the Text property when invalid) the ValidationSummary does not display the ErrorMessage property, or anything, when validation fails. Interestingly, it appears that it fails to even display the Text property when I add another control with a validator to the page. What am I doing wrong?
public class RequiredCheckBoxListValidator : BaseValidator
{
private CheckBoxList _list;
private int _requiredCount = 1;
public int RequiredCount
{
get { return _requiredCount; }
set { _requiredCount = value; }
}
public RequiredCheckBoxListValidator()
{
EnableClientScript = false;
}
protected override bool ControlPropertiesValid()
{
Control control = FindControl(ControlToValidate);
if (control != null)
{
_list = (CheckBoxList)control;
return (_list != null);
}
else
{
return false;
}
}
protected override bool EvaluateIsValid()
{
return (_list.Items.Cast<ListItem>().Where(li => li.Selected).Count() == _requiredCount);
}
}
It would help to see your clientside info.
Without that, my guesses are to check ShowSummary on the validtorsummary to make sure it is not hiding the summary, and to see if the validators and summary are in separate UpdatePanels.