I want to listen to changes of the DependencyProperty. This code works, but after every reload page with the CustomControl is callback method called multiple times...
public partial class CustomControl : UserControl
{
public CustomControl()
{
InitializeComponent();
}
public bool IsOpen
{
get { return (bool)GetValue(IsOpenProperty); }
set { SetValue(IsOpenProperty, value); }
}
public static readonly DependencyProperty IsOpenProperty =
DependencyProperty.Register("IsOpen", typeof(bool), typeof(CustomControl), new PropertyMetadata(IsOpenPropertyChangedCallback));
private static void IsOpenPropertyChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
Debug.WriteLine("Fire!");
}
}
Update
ViewModel
private bool _isOpen;
public bool IsOpen
{
get { return this._isOpen; }
set { this.Set(() => this.IsOpen, ref this._isOpen, value); } // MVVM Light Toolkit
}
View
<local:CustomControl IsOpen="{Binding Path=IsOpen}" />
Sample
project
tap "second page"
tap "true" (look at the output window)
go back
tap "second page"
tap "false" (look at the output window)
This solved my problem.
this.Unloaded += CustomControlUnloaded;
private void CustomControlUnloaded(object sender, RoutedEventArgs e)
{
this.ClearValue(CustomControl.IsOpenProperty);
}
It sounds as though the number of times the event is triggered relates to the number of times that you open the page with the control on it. This would suggest that you have multiple instances of the page.
The issue then is really that your pages are doing something that stop them from being destroyed correctly.
Unfortunately, without being able to see the code it's impossible to say what is causing this. It's probably that you've subscribed to an event in code and not unsubscribed to it though. (I see that a lot in Phone apps.)
What's happening is that the SecondPageView is being loaded multiple times. Each time a new instance is created, it binds to the data context and retrieves the value of IsOpen from the view model. Then the dependency property is set.
This is actually the desired behavior. If the properties were not set again, the view model's state would not be reflected in the page. There is no way to forward-navigate to an old page instance using the phone's native navigation API.
Related
I have a WPF project which has a main window containing a tab control which, in turn, contains several user controls.
I need the main window to display a warning when the value of certain properties have changed on the various user controls. I am using an event handler to populate a TextBlock when these properties change.
The remaining problem I have is that when the application is started, and the different UI properties get assigned an initial value, this assignment seems to be triggering the event. I only want to display this warning when the user has changed something.
Here's what I have:
An abstract class which facilitates the event
public class ViewModelBase {
Boolean isBusy;
protected virtual void OnConfigurationChanged(EventArgs args) {
EventHandler handler = ConfigurationChanged;
handler?.Invoke(this, args);
}
public event EventHandler ConfigurationChanged;
}
User controls which trigger the event when applicable properties change
class MyViewModel : ViewModelBase {
String myTextField;
public MyTextField {
get => myTextField;
set {
myTextField = value;
OnConfigurationChanged(null);
}
}
}
In my main window, I instantiate the user control and subscribe to the event. I also have the unsaved changes property in here
class MainWindowVM : ViewModelBase {
String unSavedChanges;
public MainWindowVM() {
MyViewModel MyVM = new MyViewModel();
MyVM.ConfigurationChanged += onConfigurationChanged;
}
String UnsavedChanges {
get => unSavedChanges;
set {
unSavedChanges = value;
OnPropertyChanged(nameof(UnsavedChanges));
}
}
void onConfigurationChanged(Object sender, EventArgs args) {
UnsavedChanges = "Configuration not saved.";
}
}
A XAML TextBlock bound to UnsavedChanges
<TextBlock Text="{Binding UnsavedChanges}"
Foreground="Red"
Margin="10"/>
I guess this is obvious to most folks, but for anyone else that struggles with this issue:
In order for an initial value to be reflected in a property-bound UI component, the value must be assigned to the underlying field in the object constructor. Changes to the field outside of the constructor will not be seen by the corresponding property and, in turn, will have no effect on the property-bound UI component.
Assigning a value to a property which invokes OnPropertyChanged() on set will always trigger the subscribed event. The constructor is not exempt in this regard.
I've encountered weird issue, which I cannot understand. In main page I've only one button which navigates to second page and holds my model:
public class Model : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaiseProperty(string property) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
private int index = 0;
public int Index
{
get { Debug.WriteLine($"Getting value {index}"); return index; }
set { Debug.WriteLine($"Setting value {value}"); index = value; RaiseProperty(nameof(Index)); }
}
}
public sealed partial class MainPage : Page
{
public static Model MyModel = new Model();
public MainPage()
{
this.InitializeComponent();
SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = AppViewBackButtonVisibility.Visible;
SystemNavigationManager.GetForCurrentView().BackRequested += (s, e) => { if (Frame.CanGoBack) { e.Handled = true; Frame.GoBack(); } };
}
private void Button_Click(object sender, RoutedEventArgs e) => Frame.Navigate(typeof(BlankPage));
}
On second page there is only ComboBox which has two way binding in SelectedIndex:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ComboBox SelectedIndex="{x:Bind MyModel.Index, Mode=TwoWay}">
<x:String>First</x:String>
<x:String>Second</x:String>
<x:String>Third</x:String>
</ComboBox>
</Grid>
public sealed partial class BlankPage : Page
{
public Model MyModel => MainPage.MyModel;
public BlankPage()
{
this.InitializeComponent();
this.Unloaded += (s, e) => Debug.WriteLine("--- page unloaded ---");
DataContext = this;
}
}
Nothing extraordinary. The problem is that I get two different outputs when I use Binding and x:Bind, but the worst is that after every new navigation to same page the property's getter (and setter in x:Bind) is called more and more times:
The old page still resides in memory and is still subscribed to property, that is understandable. If we run GC.Collect() after returning from page, we will begin from start.
But if we use old Binding with one-way and selection changed event:
<ComboBox SelectedIndex="{Binding MyModel.Index, Mode=OneWay}" SelectionChanged="ComboBox_SelectionChanged">
along with the eventhandler:
private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.RemovedItems.Count > 0 && e.AddedItems.FirstOrDefault() != null)
MyModel.Index = (sender as ComboBox).Items.IndexOf(e.AddedItems.FirstOrDefault());
}
then it will work 'properly' - only one getter and setter, no matter how many times we navigate to page before.
So my main questions are:
where this difference in one-way - two-way binding comes from?
taking into account that one-way Binding fires getter only once - is the described behavior of two-way desired/intended?
how you deal with this two-way binding in case of multiple getters/setters getting called?
A working sample you can download from here.
Actually when you use OneWay binding with the SectionChanged event, only the setter of the Index property is called after changing the selection. The getter is never reached hence you don't see multiple "Getting value ...".
But why is the getter not called??
Put a breakpoint on this line -
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
You will see that the value of PropertyChanged is null. So the Invoke method is never fired. I suspect this might be a bug in ComboBox with traditional binding set to OneWay. Whenever you change the selection, the binding is broken hence the PropertyChanged is null. If you change to use x:Bind, this problem goes away.
As you are already aware, the GC will only collect abandoned page instances when needed. So there are times when you see Index is referenced in multiple places, no matter which binding mechanism you chose.
One way to guarantee the getter and setter only get called once is to change the the NavigationCacheMode of your second Page to Enabled/Required. Doing so will ensure a single instance of the page.
Even after you navigated from and to a new BlankPage the other pages are still in the memory and still binded in your static model as #KiranPaul commented.
Now if you, as you commented, change in to no-static and still behaves the same, is because you make the same mistake. Even if it isn't static you still use the same variable from MainPage.(I think its not possible though, cause its not static)
So all the pages that are in the memory that havent GC.Collect()-ed will get the PropertyChanged event raised. Because the MyModel is always the same one.
Try this should work. Each time that you navigate to the BlankPage you instantiate a new Model and pass your index. Then when you unload the page you update the value in the MainPage.Model. That way when you leave the BlankPage you will only see a Set and a Get in Output.
public sealed partial class BlankPage : Page
{
public Model MyModel = new Model() { Index = MainPage.MyModel.Index };
public BlankPage()
{
this.InitializeComponent();
this.Unloaded += (s, e) => { MainPage.MyModel.Index = MyModel.Index; Debug.WriteLine("--- page unloaded ---"); };
DataContext = this;
}
}
Or when you leave BlankPage you can either:
call GC.Collect()
unbind the MyModel when you unload the page?
Edit:
With Binding it also does the same if you do it really fast. My guess is that the GC.Collect() is called
So I searched a bit and I found this:
Binding vs. x:Bind, using StaticResource as a default and their differences in DataContext
An answer says:
The {x:Bind} markup extension—new for Windows 10—is an alternative to {Binding}. {x:Bind} lacks some of the features of {Binding}, but it runs in less time and less memory than {Binding} and supports better debugging.
So Binding surely works different, it might calls GC.Collect() or Unbind it self??. Maybe have a look in x:Bind markup
You could try to add tracing to this binding to shed some light.
Also I would advise to swap these lines so that they look like this:
DataContext = this;
this.InitializeComponent();
It could be messing with your binding. As when you call initializeComponent it builds xaml tree but for binding I guess it uses old DataContext and then you immediately change DataContext, forcing rebinding of every property.
I'm playing around with the Vlc.DotNet library (Nuget: Vlc.Dotnet / https://github.com/ZeBobo5/Vlc.DotNet) . It's effectively a WinForms wrapper around libvlc.dll, with a very cursory implementation of a WPF control which just wraps the WinForms control in a HwndHost:
//WPF control class
public class VlcControl : HwndHost
{
//The WPF control has a property called MediaPlayer,
//which is an instance of Forms.VlcControl
public Forms.VlcControl MediaPlayer { get; private set; }
//WPF control constructor
public VlcControl()
{
MediaPlayer = new Forms.VlcControl();
}
//BuildWindowCore and DestroyWindowCore methods omitted for brevity
}
This means that if I want to bind to anything I need to jump through some hoops. If my instance of the WPF control is called MyWpfControl, I need to address everything via MyWpfVlcControl.MediaPlayer.[SomeMethod/SomeProperty] . I'd like to add some DependencyProperties to the WPF control to make binding easier. I'm having issues with properties that update on a timer and are set by the backing dll, and by the user via the WPF control.
The WinForms player has a Time property of type long, indicating elapsed media time in milliseconds. It also has an event called TimeChanged which fires continually during playback, updating the elapsed media time.
I've added an event handler to my WPF control for the TimeChanged event:
//WPF control constructor
public VlcControl()
{
MediaPlayer = new Forms.VlcControl();
MediaPlayer.TimeChanged += OnTimeChangedInternal;
}
private void OnTimeChangedInternal(object sender, VlcMediaPlayerTimeChangedEventArgs e)
{
Time = e.NewTime;
}
If I set up a DependencyProperty in my WPF control to wrap around Time it looks like this:
// Using a DependencyProperty as the backing store for Time. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TimeProperty =
DependencyProperty.Register("Time", typeof(long), typeof(VlcControl), new FrameworkPropertyMetadata(0L, new PropertyChangedCallback(OnTimeChanged)));
/// <summary>
/// Sets and gets the Time property.
/// </summary>
public long Time
{
get
{
return (long)this.GetValue(TimeProperty);
}
set
{
this.Dispatcher.Invoke((Action)(() =>
{
this.SetValue(TimeProperty, value);
}));
}
}
private static void OnTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//Tried this - bad idea!
//MediaPlayer.Time = (long)e.NewValue;
}
Time updates beautifully from libvlc.dll -> WinForms -> WPF. Unfortunately if I want to set the MediaPlayer.Time property from the WPF control I need to incorporate the commented line above in OnTimeChanged. When this is uncommented and MediaPlayer updates the Time property (as opposed to the WPF control), it gets itself into an infinite loop of TimeChanged -> SetTime -> TimeChanged -> SetTime -> ...
Is there a better way to implement this? Can I add a parameter somewhere that would indicate whether Time is being set from the WPF or WinForms code?
You might try implementing a type of indicator to prevent the infinite loop. Something like:
bool isUpdating = false;
private static void OnTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!isUpdating)
{
isUpdating = true;
MediaPlayer.Time = (long)e.NewValue;
isUpdating = false;
}
}
I've been trying for ever to try and figure this out.
Story: I have one MainWindow and 2 User Controls.
When the MainWindow loads One control is visible and the other is not.
Once the user enters their data and settings, I need to make the other form visible.
The form that is invisible at startup needs to be initialized, because it is gathering data from the WMI of the computer it is running on. It is also gathering AD Information in preparation for the user.
For some reason I cannot get one form to show the other.
I think this is what I'm supposed to be looking at:
#region Class Variable
public string ShowSideBar { get { return (String)GetValue(VisibilityProperty); } set { SetValue(VisibilityProperty, value); }}
public DependencyProperty VisibilityProperty = DependencyProperty.Register("ShowSideBar", typeof(string), typeof(UserControl), null);
#endregion
This is set in my MainWindow Class, however, I have no idea why I cannot call it from any other usercontrol.
Is there any way to just expose something like this to all my forms from my MainWindow?
public int RowSpan {
get { return Grid.GetRowSpan(DockPanel1); }
set { Grid.SetRowSpan(DockPanel1,value); }
}
Dependency properties must be static. Why is the type string? Should it not be Visibility if you wish to bind the visibility of the controls to it?
Does it have to be a dependency property? You could just use a regular property as well and implement INotifyPropertyChanged, since you are not binding this field to anything, rather binding other things to it.
For a dependency property, try something like this instead:
public static readonly DependencyProperty SideBarVisibilityProperty = DependencyProperty.Register("SideBarVisibility", typeof(Visibility), typeof(MyTemplatedControl), null);
public Visibility SideBarVisibility
{
get { return (Visibility)GetValue(SideBarVisibilityProperty); }
set { SetValue(SideBarVisibilityProperty, value); }
}
Firstly, this application would benefit from application of the MVVM pattern.
However, without taking that approach, you can still resolve the problem you have. It would be unusual for a user control to rely on knowing what its parent is. The code behind for your main window would be the better place to put this code. (Not as good as a view model... but that's another story.)
Add to the control that should cause the side bar to be made visible an event, ShowSideBar. Attach a handler in the main window, and use the handler to display the second control. No need for dependency properties here at all.
public class MyControl : UserControl
{
...
public event EventHandler ShowSideBar;
// Call this method when you need to show the side bar.
public void OnShowSideBar()
{
var s = this.ShowSideBar;
if (s != null)
{
s(this, EventArgs.Empty);
}
}
}
public class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
this.FirstControl.ShowSideBar += (s, e) =>
{
this.SecondControl.Visibility = Visibility.Visible;
}
}
}
I fixed the initlized Component but changing.
X:Class="AdminTools.MainWindow.ShowSideBar" to x:Class="AdminTools.MainWindow".
now i have an issues where
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:User="clr-namespace:AdminTools.Controls.User"
xmlns:Custom="clr-namespace:AdminTools.Controls.Custom"
xmlns:Bindings="clr-namespace:AdminTools.Functions"
x:Class="AdminTools.MainWindow"
Title="MainWindow" Height="691.899" Width="1500"
>
<Window.DataContext>
<Bindings:ShowSideBar />
</Window.DataContext>
<Bindings:ShowSideBar /> = ShowSideBar does not exist in the namespace clr-namespace:AdminTools.Functions
ShowSideBar: member names cannot be the same as their enclosing type.
I obviously don't get this somewhere.
I have created a UserControl, the bare bones of which is:
private readonly DependencyProperty SaveCommandProperty =
DependencyProperty.Register("SaveCommand", typeof(ICommand),
typeof(ctlToolbarEdit));
private readonly DependencyProperty IsSaveEnabledProperty =
DependencyProperty.Register("IsSaveEnabled", typeof(bool),
typeof(ctlToolbarEdit), new PropertyMetadata(
new PropertyChangedCallback(OnIsSaveEnabledChanged)));
public ctlToolbarEdit()
{
InitializeComponent();
}
public bool IsSaveEnabled
{
get { return (bool)GetValue(IsSaveEnabledProperty); }
set { SetValue(IsSaveEnabledProperty, value); }
}
public static void OnIsSaveEnabledChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
((ctlToolbarEdit)d).cmdSave.IsEnabled = (bool)e.NewValue;
}
#region Command Handlers
public ICommand SaveCommand
{
get { return (ICommand)GetValue(SaveCommandProperty); }
set { SetValue(SaveCommandProperty, value); }
}
private void cmdSave_Click(object sender, RoutedEventArgs e)
{
if (SaveCommand != null)
SaveCommand.Execute(null);
}
#endregion
Excellent. You can see what I am doing ... handling the click event of the button, and basically firing up the command.
The form (lets call that Form1 for the time being ... but note that this is actually a UserControl: common practice, I believe, in MVVM) that is hosting the control has the following line:
<ctl:ctlToolbarEdit HorizontalAlignment="Right" Grid.Row="1"
SaveCommand="{Binding Save}" IsSaveEnabled="{Binding IsValid}" />
This works great. I have an ICommand in my ViewModel called 'Save' and the ViewModel is correctly presenting the IsValid property.
So far so very good.
Now I want to have my new usercontrol also on Form2 (which is also a usercontrol - common practice, I believe, on MVVM). As it happens, Form1 and Form2 are on the screen at the same time.
It compiles, but I get a runtime exception:
'SaveCommand' property was already registered by 'ctlToolbarEdit'."
... leading me to believe that I don't get 'commands' at all.
Why can I not use my usercontrol in more than one place?
If I cannot, what would you suggest is another way to do this?
Very frustrating!
Thanks for any help.
Try making your dependency properties static. Otherwise it is getting re-registered every time you instantiate a new control. Your usage of the MVVM commands looks good otherwise and sounds like you have a good grasp on it.