I have a Custom Behavior attached to my ListViewItems that triggers on Loaded and DataContextChanged.
What this behavior does is traverse the VisualTree and by determining its direct parents, set the Visibility to Visible or Collapsed.
On initial load, and whenever I add / remove a ListViewItem to the ListView, it works properly.
However, some interactions only changes the Property of the ViewModel that is binded to the ListViewItem. What I want to do is, whenever this property changes, I want to still trigger the custom behavior that set Visibility for that ListViewItem only. Since the DataContext and Loaded doesn't trigger, my behavior doesn't happen.
Is there a way to do this?
This is my code for reference:
<DataTemplate x:Key="DataTemplate_Item">
<Grid x:Name="Grid_TemplateRoot">
<i:Interaction.Behaviors>
<Dovetail_UI_Register_Controls_Behaviors:SetItemVisibilityBehavior />
</i:Interaction.Behaviors>
<TextBlock Text={Binding Path="ItemName"}
</Grid>
</DataTemplate>
And the behavior:
public class OnLoadedOrDatacontextChangedBehavior<T> : OnLoadedBehavior<T> where T : FrameworkElement
{
protected override void OnAttached()
{
base.OnAttached();
TypedAssociatedObject.Loaded += ChangeVisibility();
TypedAssociatedObject.AddDataContextChangedHandler(OnDataContextChanged);
}
protected override void OnDetaching()
{
base.OnDetaching();
TypedAssociatedObject.Loaded -= ChangeVisibility();
TypedAssociatedObject.RemoveDataContextChangedHandler(OnDataContextChanged);
}
protected virtual void OnDataContextChanged(object sender, EventArgs args)
{
ChangeVisibility();
}
private void ChangeVisibility()
{
//Change visibility here
}
}
Thank you!
If you're not publishing this as a class library that needs to support data context coming in as any object whatsoever, then you can update your handler to listen for those property changes on your data context.
For example:
protected virtual void OnDataContextChanged(object sender, EventArgs args)
{
ChangeVisibility();
// Listen for any further changes which effect visibility.
INotifyPropertyChanged context = ((FrameworkElement)sender).DataContext as INotifyPropertyChanged;
context.PropertyChanged += (s, e) => ChangeVisibility();
}
You could additionally extend this further, for example if your handler methods for data context changing use DependencyPropertyChangedEventHandler, then you could cleanup that PropertyChanged handler. Also, you can watch for only specific properties in your PropertyChanged handler. Expanded example:
protected virtual void OnDataContextChanged(object sender, DependencyPropertyChangedEventHandler args)
{
ChangeVisibility();
INotifyPropertyChanged context;
// Cleanup any handler attached to a previous data context object.
context = e.OldValue as INotifyPropertyChanged;
if (context != null)
context.PropertyChanged -= DataContext_PropertyChanged;
// Listen for any further changes which effect visibility.
context = e.NewValue as INotifyPropertyChanged;
if (context != null)
context.PropertyChanged += DataContext_PropertyChanged;
}
private void DataContext_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "MyTargetProperty")
{
ChangeVisibility();
}
}
Related
This is my first foray into WPF and I've been trying to follow MVVM closely to get things right. The context here is that I've got a view that should display different sets of messages, all of which are stored in ObservableCollection<T>.
This is the code in my View (it's a UserControl that's hosted by a different view so I can navigate between different views at runtime)
The "i" namespace is xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
<ListBox ItemsSource="{Binding Messages}">
<i:Interaction.Behaviors>
<behaviours:ScrollOnNewItemBehaviour />
</i:Interaction.Behaviors>
<ListBox.ItemTemplate>
<DataTemplate DataType="entities:DisplayedUserMessage">
<!-- Removed for brevity -->
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This is the code for the behaviour (pieced together from other SO questions I was browsing while getting to grips with the concept):
public sealed class ScrollOnNewItemBehaviour : Behavior<ListBox>
{
protected override void OnAttached()
{
AssociatedObject.Loaded += OnLoaded;
AssociatedObject.Unloaded += OnUnLoaded;
}
protected override void OnDetaching()
{
AssociatedObject.Loaded -= OnLoaded;
AssociatedObject.Unloaded -= OnUnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
if (incc == null) return;
incc.CollectionChanged += OnCollectionChanged;
}
private void OnUnLoaded(object sender, RoutedEventArgs e)
{
var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
if (incc == null) return;
incc.CollectionChanged -= OnCollectionChanged;
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
var border = (Border)VisualTreeHelper.GetChild(AssociatedObject, 0);
var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
// Only scroll when we're scrolled to the bottom of the listbox
if (scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight)
{
scrollViewer.ScrollToBottom();
}
}
}
}
So here is where my specific issue comes up- the binding works just fine. When I change _selectedChannel (I've removed irrelevant code from the ViewModel below) the view updates with the new messages (_messages is a dictionary that holds the various ObservableCollection instances) and when I add new messages to them, the UI updates as well.
The problem is that at no point does the behaviour I've registered to the ListBox get triggered, which is a bit of an issue since I'm relying on it to keep things scrolled. My best guess was that maybe it doesn't support a bound ItemSource and the fact that the ItemSource is initially null (the dictionary will be populated asynchronously so there is no default set) means that it doesn't get registered properly/needs to be re-registered every time the binding updates?
public MessagesViewModel : ViewModelBase
{
private ObservableCollection<DisplayedUserMessage> _displayedMessages;
private Channel _selectedChannel;
public IList<DisplayedUserMessage> Messages
{
get
{
return _displayedMessages;
}
set
{
if (_displayedMessages == value)
{
return;
}
_displayedMessages = value;
NotifyPropertyChanged();
}
}
public Channel SelectedChannel
{
get
{
return _selectedChannel;
}
set
{
if (_selectedChannel == value)
{
return;
}
_selectedChannel = value;
Messages = _messages[_selectedChannel.Id];
NotifyPropertyChanged();
}
}
}
The behaviour works if it gets executed (I've verified that it doesn't with breakpoints), so if anyone has an idea regarding what I should change to make this work with changing ItemSources, please let me know!
You can subscribe to the PropertyChanged event of AssociatedObject.DataContext and wait for the Messages property to change to a valid value.
Or you initialize Messages with an empty collection and once the items are created, add them to Messages (this would raise the CollectionChanged for each item added).
A third solution is to subscribe to the ItemsControl.ItemContainerGenerator.ItemsChanged event. It is raised whenever the ItemsControl.Items property or the collection changes.
You can use this event instead of relying on the ItemsSource binding source to implement INotifyCollectionChanged. You also don't need to require the DataContext to implement INotifyPropertyChanged.
This will make your behavior more generic and reusable as it can now work with any ItemsControl.
This means listening to ItemsControl.ItemContainerGenerator.ItemsChanged is equal to INotifyCollectionChanged.CollectionChanged:
private void OnLoaded(object sender, RoutedEventArgs e)
{
var itemsControl = AssociatedObject as ItemsControl;
if (itemsControl == null) return;
itemsControl.ItemContainerGenerator.ItemsChanged += OnCollectionChanged;
}
private void OnCollectionChanged(object sender, ItemsChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
var border = (Border)VisualTreeHelper.GetChild(AssociatedObject, 0);
var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
// Only scroll when we're scrolled to the bottom of the listbox
if (scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight)
{
scrollViewer.ScrollToBottom();
}
}
}
In version 5 of MvvmCross, there has been added an asynchronous Initialize override where you can do you heavy data loading.
public override async Task Initialize()
{
MyObject = await GetObject();
}
Is there a way to determine in the View that the Initialize has completed? Say in the View I want to set the Toolbar Title to a display a field in MyObject
MyViewModel vm;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
// Create your application here
this.SetContentView(Resource.Layout.MyView);
var toolbar = (Toolbar)FindViewById(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
vm = (MyViewModel)this.ViewModel;
SupportActionBar.Title = vm.MyObject.Name;
}
On the line that sets the SupportActionBar.Title, is there a way to know for sure whether the Initialize task has completed and if not, get notified when it does?
UPDATE:
I tried set two correct answers because #nmilcoff answered my actual question and #Trevor Balcom showed me a better way to do what I wanted.
Yes, you can subscribe to InitializeTask's property changes.
Something like this will work:
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
// your code
ViewModel.PropertyChanged += MyViewModel_PropertyChanged;
}
private void MyViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if(e.PropertyName == nameof(ViewModel.InitializeTask) && ViewModel.InitializeTask != null)
{
ViewModel.InitializeTask.PropertyChanged += ViewModel_InitializeTask_PropertyChanged;
}
}
private void ViewModel_InitializeTask_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if(e.PropertyName == nameof(ViewModel.InitializeTask.IsSuccessfullyCompleted))
SupportActionBar.Title = ViewModel.MyObject.Name;
}
Of course, it could be the case that it might be easier to just listen to ViewModel.MyObject.Name property changes. But the above is a generic way to listen to InitializeTask property changes.
You can learn more about InitializeTask and MvxNotifyTask in the official documentation.
On Xamarin Forms:
I wanted to add Property Changed event login in the VM to be able to test it, so:
View.xaml.cs
protected override void OnViewModelSet()
{
base.OnViewModelSet();
var vm = this.DataContext as SearchMovieViewModel;
if (vm is null)
{
return;
}
vm.OnViewModelSet();
}
On your ViewModel:
/// <summary>
/// This method should be called in every View Code Behind when you
/// need to subscribe to InitializeTask changes.
/// </summary>
public void OnViewModelSet()
{
if (this.InitializeTask is null)
{
return;
}
this.InitializeTask.PropertyChanged += this.InitializeTask_PropertyChanged;
}
Finally on your View Model implement whatever check you need to do for MvvmCross InitializeTask, in my case I used IsCompleted Property, but you can use whichever you need:
private void InitializeTask_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(this.InitializeTask.IsCompleted))
{
// do something
}
}
DonĀ“t forget to unsubscribe, for example when the view is destroyed. You can override this method in your View Model:
public override void ViewDestroy(bool viewFinishing = true)
{
base.ViewDestroy(viewFinishing);
this.InitializeTask.PropertyChanged -= this.InitializeTask_PropertyChanged;
}
The Toolbar also supports data binding the Title property like so:
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="#style/AppTheme.PopupOverlay"
app:MvxBind="Title MyObject.Name" />
I have a combobox which will load data in the data grid based on selection change.
On change of combobox selection i need to check if the current data in data grid is correct or not . if not correct i would like to cancel the combobox selection change.
here is my behavior class
public class ComboBoxSelectionBehaviour : Behavior<ComboBox>
{
public static readonly DependencyProperty SourceProperty = DependencyProperty.RegisterAttached(
"Source",
typeof(ViewModel),
typeof(ComboBoxSelectionBehaviour),
new PropertyMetadata(null));
public ViewModel Source
{
get { return (ViewModel)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged; ;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.SelectionChanged -= AssociatedObject_SelectionChanged;
}
private void AssociatedObject_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var combo = sender as ComboBox;
if (Source != null)
{
// Suppress the event if errors exist
if (!Source.IsDataCorrect())
{
e.Handled = true;
}
}
}
}
even after handling the event combobox selected item is getting changed.
Please give some suggestions to solve this issue.
Could you simply just do a
myComboBox.SelectedIndex--
If the data is not correct? Or would this cause an infinite loop?
I'm new to WPF. Currently, I want to allow my Add button to add item by using either single click or double click. However, when I try to double click, it ends up fire single click event twice. Code in XAML as below:
<Button.InputBindings>
<MouseBinding Command="{Binding Path=AddCommand}" CommandParameter="{Binding}" MouseAction="LeftClick" />
<MouseBinding Command="{Binding Path=AddCommand}" CommandParameter="{Binding}" MouseAction="LeftDoubleClick" />
I found solution online which is to use DispatcherTimer in order to solve the problem. I have inserted these in code behind:
private static DispatcherTimer myClickWaitTimer =
new DispatcherTimer(
new TimeSpan(0, 0, 0, 1),
DispatcherPriority.Background,
mouseWaitTimer_Tick,
Dispatcher.CurrentDispatcher);
private void btnAdd_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
// Stop the timer from ticking.
myClickWaitTimer.Stop();
// Handle Double Click Actions
}
private void btnAdd_Click(object sender, RoutedEventArgs e)
{
myClickWaitTimer.Start();
}
private static void mouseWaitTimer_Tick(object sender, EventArgs e)
{
myClickWaitTimer.Stop();
// Handle Single Click Actions
}
So here comes my question. I've removed the MouseBinding in XAML and want to call for AddCommand in code behind but I'm having problem to do so due to the PrismEventAggregator. The AddCommand in .cs as below:
private void AddCommandExecute(Object commandArg)
{
// Broadcast Prism event for adding item
this.PrismEventAggregator.GetEvent<AddItemEvent>().Publish(
new AddItemPayload()
{
BlockType = this.BlockType
}
);
}
Hence would like to know how to call for the AddCommand (which is a Prism Event in .cs) in Code behind?
Note: The button is inside resource dictionary thus I failed to use the button name to call for the command.
You need to create a class which will subscribe to the event you are publishing and then execute the logic you want.
For example:
public class AddItemViewModel : INotifyPropertyChanged
{
private IEventAggregator _eventAggregator;
public AddItemViewModel(IEventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
_eventAggregator.GetEvent<AddItemEvent>().Subscribe(AddItem);
}
private void AddItem(AddItemPayload payload)
{
// Your logic here
}
}
Then when you publish the event it will trigger the subscriber and execute.
Using Expression Blend SDK, you can create a Behavior that encapsulates all your custom logic. This behavior will offer two dependency properties for your command and its parameter, so you can easily create Bindings for them, exactly as you do this for your InputBindings.
Move your event handlers and DispatcherTimer logic into this behavior:
using System.Windows.Interactivity;
class ClickBehavior : Behavior<Button>
{
// a dependency property for the command
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(ICommand),
typeof(ClickBehavior), new PropertyMetadata(null));
// a dependency property for the command's parameter
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register("CommandParameter", typeof(object),
typeof(ClickBehavior), new PropertyMetadata(null));
public ICommand Command
{
get { return (ICommand)this.GetValue(CommandProperty); }
set { this.SetValue(CommandProperty, value); }
}
public object CommandParameter
{
get { return this.GetValue(CommandParameterProperty); }
set { this.SetValue(CommandParameterProperty, value); }
}
// on attaching to a button, subscribe to its Click and MouseDoubleClick events
protected override void OnAttached()
{
this.AssociatedObject.Click += this.AssociatedObject_Click;
this.AssociatedObject.MouseDoubleClick += this.AssociatedObject_MouseDoubleClick;
}
// on detaching, unsubscribe to prevent memory leaks
protected override void OnDetaching()
{
this.AssociatedObject.Click -= this.AssociatedObject_Click;
this.AssociatedObject.MouseDoubleClick -= this.AssociatedObject_MouseDoubleClick;
}
// move your event handlers here
private void AssociatedObject_Click(object sender, RoutedEventArgs e)
{ //... }
private void AssociatedObject_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{ //... }
// call this method in your event handlers to execute the command
private void ExecuteCommand()
{
if (this.Command != null && this.Command.CanExecute(this.CommandParameter))
{
this.Command.Execute(this.CommandParameter);
}
}
The usage is very simple. You need to declare your additional namespaces:
<Window
xmlns:local="Your.Behavior.Namespace"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
...
Finally, attach the behavior to the button:
<Button>
<i:Interaction.Behaviors>
<local:ClickBehavior Command="{Binding AddCommand}" CommandParameter="{Binding}"/>
</i:Interaction.Behaviors>
</Button>
I'm working on a custom WPF UserControl and having an issue with one of my DependencyProperties.
So I built a test scenario that looks like this. In the Custom Control..
public static readonly DependencyProperty MyCollectionItemsSourceProperty = DependencyProperty.Register("DynamicHeaderItemsSource", typeof(IEnumerable), typeof(TestUserControl1),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, new PropertyChangedCallback(OnMyCollectionItemsSourceChanged)));
public IEnumerable MyCollectionItemsSource
{
get { return (IEnumerable)GetValue(MyCollectionItemsSourceProperty ); }
set { SetValue(MyCollectionItemsSourceProperty , value); }
}
protected static void OnMyCollectionItemsSourceChanged(DependencyObject property, DependencyPropertyChangedEventArgs args)
{
System.Diagnostics.Debug.WriteLine("MyCollection Updated");
}
In my test window's code behind:
public ObservableCollection<string> MyTestStrings { get; set; }
private void Window_Loaded(object sender, RoutedEventArgs e)
{
MyTestStrings.Add("First");
MyTestStrings.Add("Second");
MyTestStrings.Add("Third");
}
And in my test window's XAML:
<Grid>
<local:TestUserControl1 MyCollectionItemsSource="{Binding MyTestStrings}">
</Grid>
The problem is, I never get a notification of any type when the underline collection changes. The OnMyCollectionItemsSourceChanged only ever gets called once: at the beginning when the binding is set. What am I missing?
It is an expected behavior your MyCollectionItemsSource just change when it is set in XAML binding since (one time )t hen those adds in the collection is not changing your property itself (it is doing something inside of the collection).
if you want to get information about changing collection you have to first in OnMyCollectionItemsSourceChanged event test if the vale supports INotifyCollectionChanged this then register for NotifyCollectionChangedEventHandler isndie, do not forget to unregister your handler
protected static void OnMyCollectionItemsSourceChanged(DependencyObject property, DependencyPropertyChangedEventArgs args)
{
if( args.OldValue is INotifyCollectionChanged)
(args.OldValue as INotifyCollectionChanged ).CollectionChanged -= CollectionChangedHandler;
if(args.NewValue is INotifyCollectionChanged)
(args.OldValue as INotifyCollectionChanged).CollectionChanged += CollectionChangedHandler;
}
private static void CollectionChangedHandler(object sender, NotifyCollectionChangedEventArgs e)
{
//
}
The PropertyChangedCallback will be called only when the property is set (or nullified) not if there are any changes to the collection itself (adding/removing elements). To do that you will have to hook up to the CollectionChanged event. See this post: https://stackoverflow.com/a/12746855/4173996