Is PropertyChanged += LinkLabel_PropertyChanged; same as protected override void OnPropertyChanged(string propertyName = null) - xamarin

In a Xamarin template like this. I think there are two ways to check if a property has changed.
Adding PropertyChanged += LinkLabel_PropertyChanged;
Overriding, calling base
If I want to do something when more than one property has changed is there any difference between these two ways of calling a method?
public class LinkLabel : Label
{
public LinkLabel()
{
PropertyChanged += LinkLabel_PropertyChanged;
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
// Check property name and do action here
}
private void LinkLabel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// Check property name and do action here
}
}
For reference here is what I coded and I am wondering if that's a good solution:
public class LinkLabel : Label
{
public LinkLabel()
{
SetDynamicResource(Label.FontFamilyProperty, "Default-Regular");
SetDynamicResource(Label.FontSizeProperty, "LabelTextFontSize");
SetDynamicResource(Label.TextColorProperty, "LinkLabelColor");
VerticalOptions = LayoutOptions.CenterAndExpand;
VerticalTextAlignment = TextAlignment.Center;
}
public static readonly BindableProperty IsImportantProperty =
BindableProperty.Create(nameof(IsImportant), typeof(bool), typeof(LinkLabel), false);
public bool IsImportant
{
get { return (bool)GetValue(IsImportantProperty); }
set { SetValue(IsImportantProperty, value); }
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == IsEnabledProperty.PropertyName ||
propertyName == IsImportantProperty.PropertyName)
{
if (this.IsEnabled) {
if (this.IsImportant)
this.SetDynamicResource(Label.TextColorProperty, "LinkLabelImportantColor");
else
this.SetDynamicResource(Label.TextColorProperty, "LinkLabelColor");
}
else
this.SetDynamicResource(Label.TextColorProperty, "LinkLabelDisabledColor");
}
}
}

Yes, the difference is that registering for the PropertyChanged event works from outside, overriding the protected(!) OnPropertyChanged method works only from within derived classes of Label.
So you would normally only create a new derived LinkLabel class if you want to change the behavior of the label. There, you'd override the OnPropertyChanged (if you need to).
If you want to get informed about a change in your main form, you would register the event directly there. No need to create a derived class.

Related

DataBinding issue in Xamarin Forms

I have a new Xamarin Forms 5 app and I'm having trouble with data binding.
First, I display a message that tells the user how many items are in his list. Initially, this is 0. It's displayed by DisplayMessage property of the view model.
Then, the Init() method gets called and once the API call is finished, there are some items in MyList. I put break points to make sure that the API call works and I end up with some data in MyList property.
Because I change the value of message in my Init() method, I was expecting the message to change and display the number of items in the list but it's not changing even though I have some items in MyList.
I created a new ViewModel that looks like this:
public class MyViewModel : BaseViewModel
{
public List<MyItem> MyList { get; set; } = new List<MyItem>();
string message = "You have no items in your list... ";
public string DisplayMessage
{
get => message;
set
{
if(message == value)
return;
message = value;
OnPropertyChanged();
}
}
public async void Init()
{
var data = await _myService.GetData();
if(data.Count > 0)
message = $"You have {data.Count} items in your list!";
MyList = data;
}
}
My MainPage code behind looks like this:
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MainPage : ContentPage
{
MyViewModel _vm;
MainPage()
{
InitializeComponent();
_vm = new MyViewModel();
this.BindingContext = _vm;
}
protected override void OnAppearing()
{
base.OnAppearing();
_vm.Init();
}
}
I didn't change anyting in the base view model, except I added my service and it looks like this:
public class BaseViewModel : INotifyPropertyChanged
{
public IMyApiService MyApi => DependencyService.Get<IMyApiService>();
bool isBusy = false;
public bool IsBusy
{
get { return isBusy; }
set { SetProperty(ref isBusy, value); }
}
string title = string.Empty;
public string Title
{
get { return title; }
set { SetProperty(ref title, value); }
}
protected bool SetProperty<T>(ref T backingStore, T value,
[CallerMemberName] string propertyName = "",
Action onChanged = null)
{
if (EqualityComparer<T>.Default.Equals(backingStore, value))
return false;
backingStore = value;
onChanged?.Invoke();
OnPropertyChanged(propertyName);
return true;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
I'd appreciatae someone telling me where my mistake is. Thanks.
Without seeing the Xaml, I can't 100% answer, but here are a couple of things I see:
You are setting the "message" through the field, not the property. Since you are setting the field directly the OnPropertyChanged event isn't firing so the UI isn't getting notified that the value has changed.
I am guessing you are binding "MyList" to some sort of CollectionView or something? If it's a readonly view, using a List is ok as the collection is never updated. However, if you plan on adding or removing items at runtime, it needs to be an "ObservableCollection" for the same reason as above, the UI isn't notified of new items in a List, but an ObservableCollection will notify the UI of changes to it, so it can update.
Is what Jason mentions above in his comment. The MyList property should be setup like the other properties with the OnPropertyChanged.

How can I check the setting of a bindable property inside an IsPropertyChanged method in Xamarin.Forms?

I have this code that sets a label to one or another color depending on a property called IsEnabled:
public class LinkLabel : Label
{
public LinkLabel()
{
PropertyChanged += LinkLabel_PropertyChanged;
SetDynamicResource(Label.TextColorProperty, "LinkLabelColor");
}
private void LinkLabel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsEnabled")
{
var label = sender as LinkLabel;
var newValue = label.IsEnabled;
if ((bool)newValue)
this.SetDynamicResource(Label.TextColorProperty, "LinkLabelColor");
else
this.SetDynamicResource(Label.TextColorProperty, "LinkLabelDisabledColor");
}
}
}
What I would like to do is to add another property called IsImportant
so that if that is set to true and IsEnabled = true, then the LinkLabelColor would be set to red
Can someone give me some suggestions on how I can do this? I do know how to add things like bindable properties but I am not sure in this case how to combine it with the code that I already have?
Add a new property
public static readonly BindableProperty IsImportantProperty = BindableProperty.Create(nameof(IsImportant), typeof(bool), typeof(CustomLabel), false,/*Can remove property changed as well*/ propertyChanged: IsImportantPropertyChanged);
public bool IsImportant
{
get { return (bool)GetValue(IsImportantProperty); }
set { SetValue(IsImportantProperty, value); }
}
New Method UPDATE
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (propertyName == IsEnabledProperty.PropertyName || propertyName == IsImportantProperty.PropertyName)
{
if (this.IsImportant && this.IsEnabled)
this.SetDynamicResource(Label.TextColorProperty, "LinkLabelColor");
else
this.SetDynamicResource(Label.TextColorProperty, "LinkLabelDisabledColor");
}
}
Some issues with the other answer not working. I revisited the code and came up with this which works good:
public class LinkLabel : Label
{
public LinkLabel()
{
SetDynamicResource(Label.FontFamilyProperty, "Default-Regular");
SetDynamicResource(Label.FontSizeProperty, "LabelTextFontSize");
SetDynamicResource(Label.TextColorProperty, "LinkLabelColor");
VerticalOptions = LayoutOptions.CenterAndExpand;
VerticalTextAlignment = TextAlignment.Center;
}
public static readonly BindableProperty IsImportantProperty =
BindableProperty.Create(nameof(IsImportant), typeof(bool), typeof(LinkLabel), false);
public bool IsImportant
{
get { return (bool)GetValue(IsImportantProperty); }
set { SetValue(IsImportantProperty, value); }
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == IsEnabledProperty.PropertyName ||
propertyName == IsImportantProperty.PropertyName)
{
if (this.IsEnabled) {
if (this.IsImportant)
this.SetDynamicResource(Label.TextColorProperty, "LinkLabelImportantColor");
else
this.SetDynamicResource(Label.TextColorProperty, "LinkLabelColor");
}
else
this.SetDynamicResource(Label.TextColorProperty, "LinkLabelDisabledColor");
}
}
}

PropertyChanged is triggered, but the view is not updated

I'm changing the label in the class constructor and it works fine, the label is updated ("0"). I'm also trying to update the label when I click in a button, but it's not working ("X"). I noticed debugging that the label value is updated, PropertyChanged is triggered, but the view doesn't change.
public class HomeViewModel : ViewModelBase
{
string playerA;
public string PlayerA
{
get
{
return playerA;
}
set
{
playerA = value;
this.Notify("playerA");
}
}
public ICommand PlayerA_Plus_Command
{
get;
set;
}
public HomeViewModel()
{
this.PlayerA_Plus_Command = new Command(this.PlayerA_Plus);
this.PlayerA = "0";
}
public void PlayerA_Plus()
{
this.PlayerA = "X";
}
}
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void Notify(string propertyName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
The name of the parameter passed in your PropertyChangedEventArgs is wrong. You are using "playerA" but the name of the (public) property is "PlayerA" (uppercase "P"). Change this.Notify("playerA"); to this.Notify("PlayerA"); or even better:
Notify(nameof(PlayerA));
You can completely get rid of passing the name of the param by adding a [CallerMemberName] attribute to the Notify() method.
protected void Notify([CallerMemberName] string propertyName = null)
This allows you to just call Notify() without parameters and the name of the changed property will automatically be used.

WP7 Developpement : How to make the program wait until the end of an EventHandler?

When my view wants the value of LogoStation, it returns null because my program has not yet executed LoadStation_Completed.
I want my program waits that LoadStation_Completed is executed before continuing.
Thx
public class Infos
{
#region propriétés
private DataServiceCollection<SyndicObject> _infosStation;
public DataServiceCollection<SyndicObject> InfosStation
{
get
{
return _infosStation;
}
set
{
_infosStation = value;
}
}
#endregion
string nameStation;
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this,
new PropertyChangedEventArgs(propertyName));
}
}
private ImageSource _logoStation;
public ImageSource LogoStation
{
get
{
return _logoStation;
}
set
{
_logoStation = value;
NotifyPropertyChanged("LogoStation");
}
}
public Infos(string station)
{
nameStation = station;
getInfos();
}
public void getInfos()
{
SyndicationContext service = new SyndicationContext(new Uri("http://test/817bee9d-faf4-4680-9d05-e41c2c90ae5a/"));
IQueryable<SyndicObject> requete = (from objectSki in service.Objects
where objectSki.NOMSTATION == nameStation
select objectSki);
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
InfosStation = new DataServiceCollection<SyndicObject>();
InfosStation.LoadCompleted += new EventHandler<LoadCompletedEventArgs>(InfoStation_LoadCompleted);
InfosStation.LoadAsync(requete);
}
);
}
void InfoStation_LoadCompleted(object sender, LoadCompletedEventArgs e)
{
LogoStation = new BitmapImage(new Uri(#"http://test/upload/" + InfosStation[0].LOGO, UriKind.Absolute));
}
}
By using the property setter you are using NotifyPropertyChanged (correctly) to tell the UI bound to LogoStation that it has been updated. This should mean that the UI will display nothing initially and then the image when the load has completed.
Without seeing your view code what you have here looks correct - apart from the fact that your Infos class doesn't inherit from INotifyPropertyChanged. This means that the event never gets sent.
Update your class definition and you should be good to go.

Prism2 Region Adapter for AvalonDock's DocumentPane and DockingPane?

Does anyone have sample code on how to create a Region Adapter for AvalonDock's DocumentPane and DockingPane?
The Markus Raufer has added two region adapters to the CompositeWpfContrib project at CodePlex that supports both DocumentPane and DockingPane.
I have used Raffaeu Bermuda snippets to support Avalon tab region adapter, but found that there is some issues are not solved:
1- It does not support Activating a a certain view (aka - tab - DockableContent), so the code Region.Activate(object view) will not work.
2- All the Tabs are active by default in the region. So Region.ActiveViews collection by default has all the views, this is not ideal, as sometimes I needed to verify if a view is active or not (you could imagine a save button on a tool bar region that executes a SaveCommand only on the current active view = tab in our case)
3- Closed views doesn't actually get closed, only hidden. Even if you set the HideOnClose = true when adding the newDockableContent, it is still not removed from Region.Views collection. This could lead to memory leaks issues.
4- If you have a previously added DockableContent in the Pane, they will not get synchronized and added to the Region.Views collection.
So here are the code I am using now, it is just a small tweak from the Selector Adapter and Selector Sync Behavior found in PRISM source code:
AvalonRegionAdapter Class:
public class AvalonRegionAdapter : RegionAdapterBase<DocumentPane>
{
public AvalonRegionAdapter(IRegionBehaviorFactory factory) : base(factory) {}
protected override void AttachBehaviors(IRegion region, DocumentPane regionTarget)
{
if (region == null) throw new System.ArgumentNullException("region");
//Add the behavior that syncs the items source items with the rest of the items
region.Behaviors.Add(AvalonDocumentSyncBehavior.BehaviorKey,
new AvalonDocumentSyncBehavior()
{
HostControl = regionTarget
});
base.AttachBehaviors(region, regionTarget);
}
protected override void Adapt(IRegion region, DocumentPane regionTarget){ }
protected override IRegion CreateRegion()
{
return new Region();
}
}
AvalonDocumentSyncBehavior Behavior Code:
public class AvalonDocumentSyncBehavior : RegionBehavior, IHostAwareRegionBehavior
{
/// <summary>
/// Name that identifies the SelectorItemsSourceSyncBehavior behavior in a collection of RegionsBehaviors.
/// </summary>
public static readonly string BehaviorKey = "AvalonDocumentSyncBehavior";
private bool updatingActiveViewsInHostControlSelectionChanged;
private Selector hostControl;
/// <summary>
/// Gets or sets the <see cref="DependencyObject"/> that the <see cref="IRegion"/> is attached to.
/// </summary>
/// <value>
/// A <see cref="DependencyObject"/> that the <see cref="IRegion"/> is attached to.
/// </value>
/// <remarks>For this behavior, the host control must always be a <see cref="Selector"/> or an inherited class.</remarks>
public DependencyObject HostControl
{
get
{
return this.hostControl;
}
set
{
this.hostControl = value as Selector;
}
}
/// <summary>
/// Starts to monitor the <see cref="IRegion"/> to keep it in synch with the items of the <see cref="HostControl"/>.
/// </summary>
protected override void OnAttach()
{
bool itemsSourceIsSet = this.hostControl.ItemsSource != null;
if (itemsSourceIsSet)
{
//throw new InvalidOperationException(Resources.ItemsControlHasItemsSourceException);
}
this.SynchronizeItems();
this.hostControl.SelectionChanged += this.HostControlSelectionChanged;
this.Region.ActiveViews.CollectionChanged += this.ActiveViews_CollectionChanged;
this.Region.Views.CollectionChanged += this.Views_CollectionChanged;
}
private void Views_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
int startIndex = e.NewStartingIndex;
foreach (object newItem in e.NewItems)
{
UIElement view = newItem as UIElement;
TabViewModel viewModel = ((UserControl)view).DataContext as TabViewModel;
if (view != null)
{
DockableContent newDockableContent = new DockableContent();
newDockableContent.Content = newItem;
//if associated view has metadata then apply it.
newDockableContent.Title = view.GetType().ToString();
if (viewModel != null)
{
//Image img = new Image();
//img.Source = new BitmapImage(new Uri(#"Resources/Alerts.png", UriKind.Relative));
newDockableContent.Title = viewModel.TabModel.Title;
newDockableContent.IsCloseable = viewModel.TabModel.CanClose;
//newContentPane.Icon = img.Source;
}
//When contentPane is closed remove from the associated region
newDockableContent.Closed += new EventHandler(newDockableContent_Closed);
newDockableContent.HideOnClose = false;
this.hostControl.Items.Add(newDockableContent);
newDockableContent.Activate();
}
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (object oldItem in e.OldItems)
{
this.hostControl.Items.Remove(oldItem);
}
}
}
void newDockableContent_Closed(object sender, EventArgs e)
{
var dockableContent = sender as DockableContent;
if(dockableContent != null)
if (this.Region.Views.Contains(dockableContent.Content))
{
this.Region.Remove(dockableContent.Content);
}
}
private void SynchronizeItems()
{
List<object> existingItems = new List<object>();
// Control must be empty before "Binding" to a region
foreach (object childItem in this.hostControl.Items)
{
existingItems.Add(childItem);
}
foreach (object view in this.Region.Views)
{
this.hostControl.Items.Add(view);
}
foreach (object existingItem in existingItems)
{
this.Region.Add(existingItem);
}
}
private void ActiveViews_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (this.updatingActiveViewsInHostControlSelectionChanged)
{
// If we are updating the ActiveViews collection in the HostControlSelectionChanged, that
// means the user has set the SelectedItem or SelectedItems himself and we don't need to do that here now
return;
}
if (e.Action == NotifyCollectionChangedAction.Add)
{
var selectedDockableContent = this.hostControl.SelectedItem as DockableContent;
if (selectedDockableContent != null
&& selectedDockableContent.Content != null
&& selectedDockableContent.Content != e.NewItems[0]
&& this.Region.ActiveViews.Contains(selectedDockableContent.Content))
{
this.Region.Deactivate(selectedDockableContent.Content);
}
var _UIElement = e.NewItems[0] as FrameworkElement;
this.hostControl.SelectedItem = _UIElement.Parent;
}
else if (e.Action == NotifyCollectionChangedAction.Remove &&
e.OldItems.Contains(this.hostControl.SelectedItem))
{
this.hostControl.SelectedItem = null;
}
}
private void HostControlSelectionChanged(object sender, SelectionChangedEventArgs e)
{
try
{
// Record the fact that we are now updating active views in the HostControlSelectionChanged method.
// This is needed to prevent the ActiveViews_CollectionChanged() method from firing.
this.updatingActiveViewsInHostControlSelectionChanged = true;
object source;
source = e.OriginalSource;
if (source == sender)
{
foreach (object item in e.RemovedItems)
{
// check if the view is in both Views and ActiveViews collections (there may be out of sync)
var dockableContent = item as DockableContent;
if (this.Region.Views.Contains(dockableContent.Content) && this.Region.ActiveViews.Contains(dockableContent.Content))
{
this.Region.Deactivate(dockableContent.Content);
}
}
foreach (object item in e.AddedItems)
{
var dockableContent = item as DockableContent;
if (this.Region.Views.Contains(dockableContent.Content) &&
!this.Region.ActiveViews.Contains(dockableContent.Content))
{
this.Region.Activate(dockableContent.Content);
}
}
}
}
finally
{
this.updatingActiveViewsInHostControlSelectionChanged = false;
}
}
}
Code on bootstrapper to configure the Adapter
protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
{
var mappings = base.ConfigureRegionAdapterMappings();
mappings.RegisterMapping(typeof(AvalonDock.DocumentPane),
this.Container.Resolve<AvalonRegionAdapter>());
return mappings;
}
Then you need the TabModel and the TabViewModel as fromRaffaeu Bermuda
public sealed class TabModel : DependencyObject
{
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
// Using a DependencyProperty as the backing store for Title. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register("Title", typeof(string), typeof(TabModel));
public bool CanClose
{
get { return (bool)GetValue(CanCloseProperty); }
set { SetValue(CanCloseProperty, value); }
}
// Using a DependencyProperty as the backing store for CanClose. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CanCloseProperty =
DependencyProperty.Register("CanClose", typeof(bool), typeof(TabModel));
public bool IsModified
{
get { return (bool)GetValue(IsModifiedProperty); }
set { SetValue(IsModifiedProperty, value); }
}
// Using a DependencyProperty as the backing store for IsModified. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsModifiedProperty =
DependencyProperty.Register("IsModified", typeof(bool), typeof(TabModel));
}
And a TabViewModel acting as a base class:
public class TabViewModel : INotifyPropertyChanged
{
private TabModel _tabModel;
public TabModel TabModel
{
get { return this._tabModel; }
set
{
this._tabModel = value;
OnPropertyChanged("TabModel");
}
}
public TabViewModel()
{
this.TabModel = new TabModel();
this.TabModel.CanClose = true;
this.TabModel.IsModified = false;
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
Let me know if you need further help, I will post a blog this in the near future.
Since the Avalon DocumentPane and DockingPane are both based on the System.Windows.Controls.Primitives.Selector you can use the default SelectorRegionAdapter in Prism.
Just base your control on DockableContent
<ad:DockableContent x:Class="DesignerWPF.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:ad="clr-namespace:AvalonDock;assembly=AvalonDock"
d:DesignHeight="300" d:DesignWidth="300" Title="dans">
<Grid>
<TextBox Text="sdfdf"></TextBox>
</Grid>
</ad:DockableContent>
on your main Shell.xmal set the regions in the dockablepane
<ad:DockingManager x:Name="dockManager" Grid.Row="1" Margin="0,4,0,0">
<ad:ResizingPanel Orientation="Horizontal">
<ad:DockablePane cal:RegionManager.RegionName="LeftRegion">
</ad:DockablePane>
<ad:DocumentPane cal:RegionManager.RegionName="DocumentRegion">
</ad:DocumentPane>
</ad:ResizingPanel>
</ad:DockingManager>
then when you initialize your presenter for your control it will get displayed in the dock.
public class UserTestControl : IModule
{
public UserTestControl(IUnityContainer container, IRegionManager regionManager)
{
Container = container;
RegionManager = regionManager;
}
public void Initialize()
{
var addFundView = Container.Resolve<UserControl1>();
RegionManager.Regions["LeftRegion"].Add(addFundView);
}
public IUnityContainer Container { get; private set; }
public IRegionManager RegionManager { get; private set; }
}
My advice would be to look in Microsoft.Practices.Composite.Presentation.Regions in the Prism source. Specifically, take a look at the ItemsControlRegionAdapter and use it as a template. Remember to inherit from RegionAdapterBase<>:
public class ItemsControlRegionAdapter : RegionAdapterBase<ItemsControl>
and to override ConfigureRegionAdapterMappings() in the bootstrapper. That would look something like:
protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
{
RegionAdapterMappings mappings = base.ConfigureRegionAdapterMappings();
mappings.RegisterMapping(typeof(Canvas), Container.Resolve<CanvasRegionAdapter>());
return mappings;
}

Resources