How to make GradientStopCollection Observable? - xaml

i have run into a problem where i want to show a list of gradient stops in a listbox. The problem is that putting the gradientstops in a collection of type ObservableCollection works, but using a GradientStopCollection does not.
When i Use GradientStopCollection, the items that are in the list before the window is initialized are shown, but when a button is pressed to add a third item, the UI is not updated.
Calling OnPropertyChanged does not result in the UI being updated. I have made a small example to try to reproduce the problem.
So how can get the window to correctly update even when i use a gradientstop collection?
using System.Windows;
using System.Windows.Media;
namespace WpfApp1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
DataContext = new ViewModel();
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
ViewModel vm = (DataContext as ViewModel);
vm.Collection.Add(new GradientStop(Colors.Red, 0.5));
//This line has no effect:
vm.OnPropertyChanged("Collection");
}
}
}
Viewmodel:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
namespace WpfApp1
{
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public GradientStopCollection Collection
{
get
{
return collection;
}
set
{
collection = value;
}
}
//Replacing GradientStopCollection
// with ObservableCollection<GradientStop> makes it work
GradientStopCollection collection;
public ViewModel()
{
GradientStop a = new GradientStop(Colors.Green, 0);
GradientStop b = new GradientStop(Colors.Yellow, 1.0);
collection = new GradientStopCollection() { a, b } ;
OnPropertyChanged("Collection");
}
public void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
handler?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
public class Converter : IValueConverter
{
public object Convert(object value, Type targettype, object parameter, CultureInfo cultureInfo)
{
if (value is Color color)
return new SolidColorBrush(color);
return Binding.DoNothing;
}
public object ConvertBack(object value, Type targettype, object parameter, CultureInfo cultureInfo)
{
throw new NotImplementedException();
}
}
}
And finally the xaml:
<Grid>
<Grid.Resources>
<local:Converter x:Key="ColorConverter"/>
<DataTemplate DataType="{x:Type GradientStop}">
<TextBlock
Width="50"
Background="{Binding Color, Converter={StaticResource ColorConverter}}"
Text="block"
/>
</DataTemplate>
</Grid.Resources>
<ListBox
x:Name="GradientListBox"
Width="72"
Height="92"
ItemsSource="{Binding Collection}" />
<Button Content="Button" HorizontalAlignment="Left" Margin="169,264,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click"/>
</Grid>

I don't think that there is any easy way around this problem.
You could create your own collection class, inheriting from GradientStopCollection and implementing the interface INotifyCollectionChanged, effectively making an ObservableGradientStopCollection.
You can probably find an implementation of INotifyCollectionChanged as an excmple.
It might be easier, just to keep two collections, although it seems like bad style.

Related

Binding fires on unloaded view/view-model when creating a new view

If I bind a RadioButton to a view-model property using a type converter, every time I create a view, the setter on the previous ViewModel gets called, even though the view is Unloaded and should not exist anymore. Here is the minimum code to reproduce the issue:
1) Define an enum type:
enum EnumType {
Value1,
Value2,
}
2) Define a convereter:
public class EnumTypeToBooleanConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, string language) {
return true;
}
public object ConvertBack(object value, Type targetType, object parameter, string language) {
return EnumType.Value1;
}
}
3) Define a view-model:
class ViewModel : INotifyPropertyChanged {
private EnumType value;
public ViewModel() {
Debug.WriteLine(string.Format("ViewModel ({0})::ctor", this.GetHashCode()));
}
public EnumType Value {
get {
Debug.WriteLine(string.Format("ViewModel ({0})::Value::get", this.GetHashCode()));
return this.value;
}
set {
Debug.WriteLine(string.Format("ViewModel ({0})::Value::set", this.GetHashCode()));
if (this.value != value) {
this.value = value;
this.OnPropertyChanged();
}
}
}
private void OnPropertyChanged([CallerMemberName] string name = null) {
if (this.PropertyChanged != null) {
var ea = new PropertyChangedEventArgs(name);
this.PropertyChanged(this, ea);
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
4) Define a UserControl (View.xaml)
<UserControl
x:Class="BindingIssue.View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:BindingIssue"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400"
x:Name="root">
<UserControl.DataContext>
<local:ViewModel x:Name="ViewModel"/>
</UserControl.DataContext>
<Grid>
<ScrollViewer>
<StackPanel>
<RadioButton x:Name="rdo1"
Content="Value1"
IsChecked="{Binding Path=Value, Converter={StaticResource EnumTypeToBooleanConverter}, ConverterParameter=Value1, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Button x:Name="btnClose"
Click="btnClose_Click"
Content="Close"/>
</StackPanel>
</ScrollViewer>
</Grid>
5) Add code behind of the View:
public View() {
Debug.WriteLine(string.Format("View ({0})::ctor", this.GetHashCode()));
this.InitializeComponent();
this.Loaded += OnLoaded;
this.Unloaded += OnUnloaded;
}
private void btnClose_Click(object sender, RoutedEventArgs e) {
if (this.Parent is Popup) {
Debug.WriteLine("Closing the popup...");
((Popup)this.Parent).IsOpen = false;
}
}
private void OnLoaded(object sender, RoutedEventArgs e) {
Debug.WriteLine(string.Format("View ({0})::Loaded", this.GetHashCode()));
}
private void OnUnloaded(object sender, RoutedEventArgs e) {
Debug.WriteLine(string.Format("View ({0})::Unloaded", this.GetHashCode()));
}
6) MainPage (XAML)
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
x:Name="Grid">
<Button x:Name="btnNewView"
Click="btnNewView_Click"
Content="New View"
Margin="4"/>
</Grid>
7) Add the event handler to the MainPage
private void btnNewView_Click(object sender, RoutedEventArgs e) {
Debug.WriteLine("Opening a new popup...");
View view = new View();
view.HorizontalAlignment = HorizontalAlignment.Center;
view.VerticalAlignment = VerticalAlignment.Center;
Popup popup = new Popup();
popup.Child = view;
popup.HorizontalOffset = 300;
popup.VerticalOffset = 300;
popup.IsOpen = true;
}
Opening and closing popups multiple times results the following output (Please keep track of hash codes):
Opening a new popup...
View (46418718)::ctor
ViewModel (59312528)::ctor
ViewModel (59312528)::Value::get
View (46418718)::Loaded
Closing the popup...
View (46418718)::Unloaded
Opening a new popup...
View (58892413)::ctor
ViewModel (61646925)::ctor
ViewModel (61646925)::Value::get
ViewModel (59312528)::Value::set
View (58892413)::Loaded
Closing the popup...
View (58892413)::Unloaded
Which means the setter for the ViewModel that is created in the Unloaded view model is being called that is a little bit strange. This behavior is the same for both x:Bind and Binding.
I would like to know if there is an explanation on this behavior.
To Clarify more:
A brand new pair of view/view-model instances are created each time but when the new view is being loaded, the setter on the previous instance of view-model is being called. The previous instance of the view is unloaded and should not even exist at that point. (Think of a popup that is being closed each time, and there is not event a reference the old view/view-model.)
Which means the setter for the ViewModel that is created in the Unloaded view
model is being called that is a little bit strange
Firstly, the setter is not called when the view unloaded, it is called when loading the view. You can add the Loading event handle to verify this. Adding loading event code to the code behind of view control as follows:
this.Loading += View_Loading;
private void View_Loading(FrameworkElement sender, object args)
{
Debug.WriteLine(string.Format("View ({0})::Loading", this.GetHashCode()));
}
And the output now will be:
Closing the popup...
View (22452836)::Unloaded
Opening a new popup...
View (58892413)::ctor
ViewModel (61646925)::ctor
View (58892413)::Loading
ViewModel (61646925)::Value::get
ViewModel (19246503)::Value::set
View (58892413)::Loaded
Secondly, we need to look into why setter is called in this scenario.
One is because you set the binding mode to TwoWay. If you remove this property as follows you will not see the setter called since the ViewModel doesn't need to know the changes in the view.
<RadioButton x:Name="rdo1" Content="Value1" IsChecked="{Binding Path=Value, Converter={StaticResource EnumTypeToBooleanConverter}, ConverterParameter=Value1, UpdateSourceTrigger=PropertyChanged}"/>
More details about binding mode please reference this article. Another reason may be the specific for RadioButton control. A RadioButton can be cleared by clicking another RadioButton in the same group, but it cannot be cleared by clicking it again. So when set IsChecked property to true, we thought the property value of the group is updated. This will trigger the TwoWay binding. In your scenrio, you can test this by setting the default value of IsChecked to false as follows, and you will find the setter is not called until you select the rdo1 on the UI. Or you can use another control CheckBox for testing which will also not call the setter until IsChecked value updated.
public class EnumTypeToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
return EnumType.Value1;
}
}
The behavior is NOT the same if ScrollViewer gets removed from the View
The behavior is NOT the same for lets say a Boolean property
For these two scenarios, I also tested on my side. The result is the same with the outputs above. Since I don't know how you bind the Boolean property, as I mentioned, whether setter is called depend on what the binding mode is and whether you set or update the property. My testing code about binding Boolean is as follows which have same outputs.
View.xaml
<RadioButton x:Name="rdo2"
Content="BoolValue"
IsChecked="{Binding Path=BoolValue, Converter={StaticResource EnumTypeToBooleanConverter}, ConverterParameter=Value1, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
Converter:
public class EnumTypeToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return true;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
//return EnumType.Value1;
return true;
}
}
ViewModel;
private bool boolvalue;
public bool BoolValue
{
get
{
Debug.WriteLine(string.Format("ViewModel ({0})::boolvalue::get", this.GetHashCode()));
return this.boolvalue;
}
set
{
Debug.WriteLine(string.Format("ViewModel ({0})::boolvalue::set", this.GetHashCode()));
if (this.boolvalue != value)
{
this.boolvalue = value;
this.OnPropertyChanged();
}
}
}

Use RelayCommand with not only buttons

I am using MVVM Light in my project and I am wondering if there is any way to use RelayCommand with all controls (ListView or Grid, for example).
Here is my current code:
private void Item_Tapped(object sender, TappedRoutedEventArgs e)
{
var currentItem = (TechItem)GridControl.SelectedItem;
if(currentItem != null)
Frame.Navigate(typeof(TechItem), currentItem);
}
I want to move this code to Model and use RelayCommand, but the ListView, Grid and other controls don't have Command and CommandParameter attributes.
What does MVVM Light offer to do in such cases?
Following on from the link har07 posted this might be of some use to you as I see you mention CommandParameter.
It is possible to send the "Tapped" item in the list to the relay command as a parameter using a custom converter.
<ListView
x:Name="MyListView"
ItemsSource="{Binding MyCollection}"
ItemTemplate="{StaticResource MyTemplate}"
IsItemClickEnabled="True">
<i:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="ItemClick">
<core:InvokeCommandAction Command="{Binding ViewInMoreDetail}" InputConverter="{StaticResource TapConverter}" />
</core:EventTriggerBehavior>
</i:Interaction.Behaviors>
</ListView>
Custom converter class
public class TapConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
var args = value as ItemClickEventArgs;
if (args != null)
return args.ClickedItem;
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
In your view model you then have a relaycommand.
public RelayCommand<MyObject> MyRelayCommand
{
get;
private set;
}
In your constructor initialise the relay command and the method you want to fire when a tap happens.
MyRelayCommand = new RelayCommand<MyObject>(HandleTap);
This method receives the object that has been tapped in the listview as a parameter.
private void HandleTap(MyObject obj)
{
// obj is the object that was tapped in the listview.
}
Don't forget to add the TapConverter to your App.xaml
<MyConverters:TapConverter x:Key="TapConverter" />

Windows 8.1: Behaviors on Flyouts don't Work

I am developing a windows 8.1 app using VS 2013 and MVVM Light.
The following code shows the behavior in a flyout within an appbar:
<AppBarButton.Flyout>
<Flyout x:Name="FlyoutCalculator"
Placement="Top"
FlyoutPresenterStyle="{StaticResource FlyoutPresenterBaseStyle}">
<uc:Calculator ApplyCommand="{Binding CancelCommand}"
CancelCommand="{Binding CancelCommand}"
Available="{Binding AvailableCounter, Mode=OneWay}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"/>
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="Opening">
<core:InvokeCommandAction Command="{Binding ShowCurrentCostsCommand}" />
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Flyout>
</AppBarButton.Flyout>
Unfortunately I get an exception while compiling the app:
WinRT-Informationen: Cannot add instance of type Microsoft.Xaml.Interactions.Core.EventTriggerBehavior to a collection of type Microsoft.Xaml.Interactivity.BehaviorCollection
Other Behaviors in the View do work, does someone know a solution to this?
Extremely late answer here, but I had the same issue and came up with a solution after finding this post.
I just created a custom behavior specifically for flyouts, used like this. OpenActions will execute when the flyout is opened, and CloseActions will execute when the flyout closes. In this case, I wanted the bottom app bar to not be visible when the flyout was open.
<Flyout Placement="Full">
<i:Interaction.Behaviors>
<behaviors:FlyoutBehavior>
<behaviors:FlyoutBehavior.OpenActions>
<core:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" TargetObject="{Binding ElementName=CommandBar}" />
</behaviors:FlyoutBehavior.OpenActions>
<behaviors:FlyoutBehavior.CloseActions>
<core:ChangePropertyAction PropertyName="Visibility" Value="Visible" TargetObject="{Binding ElementName=CommandBar}" />
</behaviors:FlyoutBehavior.CloseActions>
</behaviors:FlyoutBehavior>
</i:Interaction.Behaviors>
<Grid>
...
</Grid>
</Flyout>
Code is here:
class FlyoutBehavior : DependencyObject, IBehavior
{
public DependencyObject AssociatedObject { get; private set; }
public void Attach(Windows.UI.Xaml.DependencyObject associatedObject)
{
var flyout = associatedObject as FlyoutBase;
if (flyout == null)
throw new ArgumentException("FlyoutBehavior can be attached only to FlyoutBase");
AssociatedObject = associatedObject;
flyout.Opened += FlyoutOpened;
flyout.Closed += FlyoutClosed;
}
public void Detach()
{
var flyout = AssociatedObject as FlyoutBase;
if (flyout != null)
{
flyout.Opened -= FlyoutOpened;
flyout.Closed -= FlyoutClosed;
}
}
public static readonly DependencyProperty OpenActionsProperty =
DependencyProperty.Register("OpenActions", typeof(ActionCollection), typeof(FlyoutBehavior), new PropertyMetadata(null));
public ActionCollection OpenActions
{
get { return GetValue(OpenActionsProperty) as ActionCollection; }
set { SetValue(OpenActionsProperty, value); }
}
public static readonly DependencyProperty CloseActionsProperty =
DependencyProperty.Register("CloseActions", typeof(ActionCollection), typeof(FlyoutBehavior), new PropertyMetadata(null));
public ActionCollection CloseActions
{
get { return GetValue(CloseActionsProperty) as ActionCollection; }
set { SetValue(CloseActionsProperty, value); }
}
private void FlyoutOpened(object sender, object e)
{
foreach (IAction action in OpenActions)
{
action.Execute(AssociatedObject, null);
}
}
private void FlyoutClosed(object sender, object e)
{
foreach (IAction action in CloseActions)
{
action.Execute(AssociatedObject, null);
}
}
public FlyoutBehavior()
{
OpenActions = new ActionCollection();
CloseActions = new ActionCollection();
}
}
I do not have a solution but:
I'm not using Flyouts in my Windows 8.1 App, I'm using a UserControl on which I have added a EventTriggerBehavior as you did. And I get exactly the same Errormessage from VisualStudio at runtime.
As I am using a RoutedEventHandler this could cause the Problem as you use
EventHandler<object> Opening
as the Trigger for the Behavior. But that is just an idea of what is the problem.
For me I have found an answer:
I have changed the Type of my RoutedEventHandler to be just a normal EventHandler. And the Method inside the CodeBehind which triggers the RoutedEventHandler is invoked with only the sender, because I dont know how to convert RoutedEventArgs into EventArgs, but as long as I dont need the EventArgs it's not a problem.
You could also make a workaround by creating a UserControl with a Flyout Control and make the Opening Event public to the Page where you use it. Then you can add the EventTriggerBehavior to the UserControl and connect it to your custom Opening Event and you should get the expected behavior.

UserControl InitializeComponents() method set the Content property, but does NOT trigger binding update

For example, there is a class MyUserControlBase derived from UserControl and within its constructor there is set of binding for Content dependency property.
MyUserControlBase.cs
namespace BindingBeforeInitComp
{
using System.Diagnostics;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Data;
public class MyUserControlBase : UserControl
{
public static readonly DependencyProperty MyContentProperty = DependencyProperty.Register("MyContent",
typeof(object),
typeof(MyUserControl),
new PropertyMetadata(null, MyContentChangedCallback));
public MyUserControlBase()
{
// Set binding to the Content property.
var propertyPath = new PropertyPath("Content");
var binding = new Binding { Path = propertyPath, Source = this };
SetBinding(MyContentProperty, binding);
}
public object MyContent
{
get { return GetValue(MyContentProperty); }
set { SetValue(MyContentProperty, value); }
}
private static void MyContentChangedCallback(
DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
Debugger.Break(); // Breaking here to show that binding to Content property has updated.
}
}
}
There is also MyUserControl(cs+xaml) derived from MyUserControlBase. In the constructor there is default IntializeComponents() method (Note: binding was set before this method in the base constructor). Before calling of IntializeComponents() the Content property is null, after calling Content is set to content described in xaml. But binding does not update a target property.
MyUserControl.xaml.cs
namespace BindingBeforeInitComp
{
using System.Diagnostics;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
public sealed partial class MyUserControl : MyUserControlBase
{
public MyUserControl()
{
Debugger.Break(); // Here Content is null.
InitializeComponent();
Debugger.Break(); // Here Content is set, but as you can see there is no binding update.
}
private void ChangeContentClick(object sender, RoutedEventArgs e)
{
// Change content by a click. In this case the property changing should trigger update.
Content = new Grid() { Background = new SolidColorBrush(Colors.Chartreuse) };
}
}
}
MyUserControl.xaml
<local:MyUserControlBase
x:Class="BindingBeforeInitComp.MyUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:BindingBeforeInitComp"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<Grid Background="OrangeRed">
<Button Click="ChangeContentClick" HorizontalAlignment="Center" Width="200" Height="150" Background="#FF00AE4F">Change content</Button>
</Grid>
</local:MyUserControlBase>
Full sources: https://github.com/inTagger/Bugs/tree/master/BindingBeforeInitComp
UPDATE: I have updated sources on the GitHub, added WPF (NET45) project to demonstrate the true way how dependency properties and bindings should work. And yes, WPF has no such problem (behavior/feature/bug).
UPDATE2: If Content dependency property is set directly in constructor - binding works, but not if Content is set somewhere inside InitializeComponents().
The InitializeComponent method's job is to load your Xaml, so it's to be expected that none of your UI elements are available until after it has run.
Consider handling the Loaded event - you can attach a handler to that in your base constructor, and it won't be raised until after the object is fully constructed.

How to bind a string from singleton in xaml (WinRT / C#)

i've created a Singleton which contains a string.
Now i want to bind this string to a TextBlock an Xaml.
<TextBlock Visibility="Visible" Text="{Binding singleton.Instance.newsString, Mode=TwoWay}"/>
When i run the WinRT App, the TextBlock-Text-String is empty.
EDIT 1:
Now it runs. But when i change the string in the singleton the TextBlock does not update.
Here is the c# code from my singleton
namespace MyApp
{
public sealed class singleton : INotifyPropertyChanged
{
private static readonly singleton instance = new singleton();
public static singleton Instance
{
get
{
return instance;
}
}
private singleton() { }
private string _newsString;
public string newsString
{
get
{
if (_newsString == null)
_newsString = "";
return _newsString;
}
set
{
if (_newsString != value)
{
_newsString = value;
this.RaiseNotifyPropertyChanged("newsString");
}
}
}
private void RaiseNotifyPropertyChanged(string property)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(property));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
In my code behind the xaml i do this
singleton.Instance.newsString = "Breaking news before init";
this.Resources.Add("newsStringResource", singleton.Instance.newsString);
this.InitializeComponent();
singleton.Instance.newsString = "Breaking news AFTER init";
and in the xaml i bind the Resource with
<TextBlock Visibility="Visible" Text="{StaticResource newsStringResource}" />
With this code the TextBlock shows "Breaking news before init".
Whats wrong now?
Add your singleton to app resources using code behind before the TextBlock is constructed and reference the singleton by key.