locked
TemplatedControl inconsistently triggers visual states

    Question

  • I would greatly appreciate some help. I can't figure out what I am doing wrong. I have a simple custom control with 2 states active and inactive. When the active state changes I want to change the visual state of the control. Some times the GotoState function works and sometimes it doesn't :(

    Here is the class:

    public sealed class SpeakerIndicator : Control
        {
            public SpeakerIndicator()
            {
                this.DefaultStyleKey = typeof(SpeakerIndicator);
            }
    
            #region DependencyProperties
            public static readonly DependencyProperty ActiveProperty = DependencyProperty.Register("Active", typeof(bool), typeof(SpeakerIndicator), 
                new PropertyMetadata(true, OnActiveValueChanged));
            public bool Active
            {
                get { return (bool)GetValue(ActiveProperty); }
                set
                {
                    SetValue(ActiveProperty, value);
                }
            }
    
            public static DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(string), typeof(SpeakerIndicator), null);
            public string Content
            {
                get { return (string)GetValue(ContentProperty); }
                set { SetValue(ContentProperty, value); }
            }
    
            public static DependencyProperty InActiveColorProperty = DependencyProperty.Register("InActiveColor", typeof(Color), typeof(SpeakerIndicator), null);
            public Color InActiveColor
            {
                get { return (Color)GetValue(InActiveColorProperty); }
                set { SetValue(InActiveColorProperty, value); }
            }
            #endregion
    
        #region Events
            private static void OnActiveValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                if (d != null)
                {
                    SpeakerIndicator si = d as SpeakerIndicator;
                    bool result = false;
                    if ((bool)e.NewValue == true)
                    {
                        result = VisualStateManager.GoToState(si, "Active", true);
                    }
                    else
                    {
                        result = VisualStateManager.GoToState(si, "InActive", true);
                    }
                    System.Diagnostics.Debug.WriteLine("Result: " + result.ToString() +
                        " Old Value: " + e.OldValue.ToString() + " New Value: " + e.NewValue.ToString());
                }
            }
        #endregion
    
            
        }

    Here is the control template:

    <Style TargetType="controls:SpeakerIndicator">
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="Foreground" Value="Green"/>
            <Setter Property="BorderBrush" Value="Green"/>
            <Setter Property="BorderThickness" Value="2"/>
            <Setter Property="InActiveColor" Value="#FF444343"/>
            <Setter Property="MinWidth" Value="24"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="controls:SpeakerIndicator">
                        <Border
                            x:Name="border"
                            Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Margin="3"
                            HorizontalAlignment="Right" VerticalAlignment="Top"
                            CornerRadius="5,5,5,5">
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="CommonStates">
                                        <VisualState x:Name="Active">
                                            <Storyboard>
                                            <ColorAnimation Duration="0" To="{TemplateBinding Foreground}" Storyboard.TargetProperty="(BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="border"/>
                                            <ColorAnimation Duration="0" To="{TemplateBinding Foreground}" Storyboard.TargetProperty="(Foreground).(SolidColorBrush.Color)" Storyboard.TargetName="textblock"/>
                                            </Storyboard>
                                        </VisualState>
                                        <VisualState x:Name="InActive">
                                            <Storyboard>
                                                <ColorAnimation Duration="0" To="#FF444343" Storyboard.TargetProperty="(BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="border"/>
                                                <ColorAnimation Duration="0" To="#FF444343" Storyboard.TargetProperty="(Foreground).(SolidColorBrush.Color)" Storyboard.TargetName="textblock"/>
                                            </Storyboard>
                                        </VisualState>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <TextBlock Text="{TemplateBinding Content}" FontSize="12" Foreground="{TemplateBinding Foreground}" Margin="2" 
                                        x:Name="textblock"
                                        TextWrapping="NoWrap" 
                                        TextAlignment="Center"
                                        MinWidth="{TemplateBinding MinWidth}"
                                        />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

    This is what is in my page:

    <Controls:SpeakerIndicator Content="A" Active="{Binding IsSpeakerAOn}"/>

    I am pulling my hair out. If you could tell me what I am doing wrong it would be greatly appreciated.

    Thanks!

    Friday, June 06, 2014 5:02 AM

Answers

  • Thanks for your help. I think I solved my problem. If I move the call which gets the device status to the MainPage LoadState event it works. I guess that all the page elements did not exist when I made the call earlier and thus the GotoState failed because the tree had not been created yet.
    • Marked as answer by jsullyboy2 Saturday, June 07, 2014 3:39 AM
    Saturday, June 07, 2014 3:38 AM

All replies

  • Hi,

    I think the problem might come from your IsSpeakerAOn property because I just pasted your code in Visual Studio 2013 and then bound the Active property to the IsChecked property of a checkbox I added and the control always goes from Active to InActive when I check and uncheck the checkbox.

    You can always post your ViewModel so i can try and see if there is anything wrong in there.

    Regards

    Friday, June 06, 2014 10:36 AM
  • Hi,

    I created a test class with a Boolean property "IsSpeakerAOn" as :

     public class MyClass : INotifyPropertyChanged
        {
            private bool _IsSpeakerAOn;
    
            public bool IsSpeakerAOn
            {
                get { return _IsSpeakerAOn; }
                set
                {
    
                    _IsSpeakerAOn = value;
                    RaisePropertyChanged("IsSpeakerAOn");
                }
            
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
            protected void RaisePropertyChanged(string name)
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs(name));
                }
            }
    
        }

    And to toggle the property values between true/false , I added a button to see the changes :

      MyClass c = new MyClass();
            public MainPage()
            {
                this.InitializeComponent();
                this.DataContext = c;
            }
    
            private void Button_Click(object sender, RoutedEventArgs e)
            {
                if (c.IsSpeakerAOn)
                    c.IsSpeakerAOn = false;
                else
                    c.IsSpeakerAOn = true;
            }

    When we start running the app, the button is already green but when you click the button for first time, it remains green. That is because the default value of the "ActiveProperty" Dependency property is "true" and the default value of the Boolean property "IsSpeakerAOn" is initialized to "false". So, if we initialize the value of "IsSpeakerAOn" to "true" , I don't see any inconsistency while changing the state.

    public MyClass()
    {
    IsSpeakerAOn = true;
    }

    Also, make sure the class that contains the member "IsSpeakerAOn" implements INotifyPropertyChanged.

    -Sagar

    Friday, June 06, 2014 11:01 AM
  • Hi,

    Again, I'm not sure what can be possibly wrong as I told you, your templated control works fine.

    I'm sending you the project on which it is perfectly working.  You can find it here : http://1drv.ms/1hDotaF

    Let me know if it isn't working accordingly

    I used Visual Studio 2013.

    Regards

    • Proposed as answer by SisuHak Friday, June 06, 2014 11:44 AM
    Friday, June 06, 2014 11:44 AM
  • Thanks for your response. The view model is the implementation of an abstract class.

    public abstract class Device : INotifyPropertyChanged
        {
            #region members
            private bool _speakerA = true;
            private bool _speakerB = true;
            #endregion
    
            #region Constructor
    
            public Device() { }
    
            #endregion
    
            #region Properties
            public bool IsSpeakerAOn
            {
                get { return _speakerA; }
                protected set { SetProperty(ref this._speakerA, value); }
            }
    
            public bool IsSpeakerBOn
            {
                get { return _speakerB; }
                set { SetProperty(ref this._speakerB, value); }
            }
    
            #region Events
            public event PropertyChangedEventHandler PropertyChanged;
            protected void OnPropertyChanged(string name)
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs(name));
                }
            }
    
            /// <summary>
            /// Uses reflection so that the property name is not needed in the call
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <param name="storage"></param>
            /// <param name="value"></param>
            /// <param name="propertyName"></param>
            /// <returns></returns>
            protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = null)
            {
                if (object.Equals(storage, value)) return false;
    
                storage = value;
                this.OnPropertyChanged(propertyName);
                return true;
            }
    
            #endregion
        }
    
    
    public class TXNR3007 : Device
        {
            #region members
            public const string DEVICE_NAME = "TX-NR3007";
            private TXNR3007MainZone _mainZone = null;
            #endregion
    
            #region Constructors
            public TXNR3007(string DeviceHostName, string Port)
                : base(DeviceHostName, Port)
            {
            }
    
            public TXNR3007()
            {
            }
            #endregion
    
            #region Properties
    
            public override string Name
            {
                get { return DEVICE_NAME; }
            }
    
        }

    I removed code from the above classes to be brief. The page binds to a Device as its view model. The reason for this is that the page can control more than one type of device because the devices implement the abstract class.

    If I changed the value of the SpeakerA or SpeakerB properties from code behind a button it works fine. The problem seems to be on binding. The first time the OnActiveValueChanged method is called the GotoState fails. This initial call must be when the control is binding to the property. Once the page is loaded and I change the value of SpakerB via code behind a button it works fine.

    Friday, June 06, 2014 12:54 PM
  • Hi Thanks for your reply. The problem seems to be on binding. In the initial call to OnActivateValueChanged (which I assume is made when the control binds to the property) GotoState always fails. My Debug output show Result: False Old Value: True New Value: false. I put a button on the page to change the value of the speakerB property and everything works as expected when I click the button.

    The Device that the DataContext is set to is a property of the Application. When the application starts it connects to the device and queries the status of the device. The MainPage then loads with the DataContext set to the Connected Device property of the Application.

    I posted the view model code above.

    Friday, June 06, 2014 1:04 PM
  • Hi,

    At which point in the application load cycle do you create an instance of your device class?

    Friday, June 06, 2014 1:33 PM
  • Hi,

    Would you mind sending me the project so i can see what's going on.  i think somewhere in the loading cycle there might be something that's setting the property wrong.  OnActivateValueChanged will be called when the attached property will be changed, regardless of the binding.

    Friday, June 06, 2014 1:38 PM
  • The device class is created in the constructor of the Application. 

    public App()
            {
                _device = new DeviceFactory().CreateDeviceController(DeviceFactory.ONKYO_TXNR3007_DEVICE, ApplicationSettings.DeviceName , 
                    ApplicationSettings.DevicePort);
                this.InitializeComponent();
                this.Suspending += this.OnSuspending;
            }

    In the OnLaunched event of the application the connection to the device and query of its status is done.

    protected async override void OnLaunched(LaunchActivatedEventArgs e) { #if DEBUG if (System.Diagnostics.Debugger.IsAttached) { this.DebugSettings.EnableFrameRateCounter = true; } #endif //load the commands for the receiver await ConnectedDevice.RefreshDeviceStatus(); Frame rootFrame = Window.Current.Content as Frame;

    ...

    The mainPage is created and navigated to in the Onlaunch method too.

    if (!rootFrame.Navigate(typeof(Pages.MainPage), e.Arguments))
                    {
                        throw new Exception("Failed to create initial page");
                    }

    The mainpage datacontext is set to the device which is a property of the application.

    public Device DefaultViewModel
            {
                get { return _app.ConnectedDevice; }
            }
    DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"


    Saturday, June 07, 2014 2:58 AM
  • Sure. How can I send it to you?
    Saturday, June 07, 2014 3:00 AM
  • Thanks for your help. I think I solved my problem. If I move the call which gets the device status to the MainPage LoadState event it works. I guess that all the page elements did not exist when I made the call earlier and thus the GotoState failed because the tree had not been created yet.
    • Marked as answer by jsullyboy2 Saturday, June 07, 2014 3:39 AM
    Saturday, June 07, 2014 3:38 AM