In a Silverlight MVVMLight 4.0 application I have a listbox, a textbox and a checkbox.
The listbox's ItemsSource is bound to a list of objects in the viewmodel.
The listbox's SelectedItem is two-way bound to an object (SelectedActivity) in the viewmodel.
Both the textbox's Text and the checkbox's IsSelected properties are two-way bound to the SelectedActivity object (Name and Selected properties) in the viewmodel.
There is no codebehind.
This works fine: changing the Name in the textbox or checking/unchecking the checkbox and then tabbing will change the underlying property of the object.
But when I change the name (or the checked state) and then immediatelly click another item in the list, the change is not registered.
Does anybody have a workaround for this?
kind regards,
Karel
This is the XAML:
<ListBox Height="251" HorizontalAlignment="Left" Margin="11,39,0,0" Name="activitiesListBox" ItemsSource="{Binding Activities.Items}" VerticalAlignment="Top" Width="139"
SelectedItem="{Binding Activities.SelectedActivity, Mode=TwoWay}">
This is the Activities class holding the items bound to the list:
public class CLJActivitiesViewModel : ViewModelBase
{
/// <summary>
/// Initializes a new instance of the ActivitiesViewModel class.
/// </summary>
public CLJActivitiesViewModel()
{
////if (IsInDesignMode)
////{
//// // Code runs in Blend --> create design time data.
////}
////else
////{
//// // Code runs "for real": Connect to service, etc...
////}
}
#region items
/// <summary>
/// The <see cref="Items" /> property's name.
/// </summary>
public const string ItemsPropertyName = "Items";
private ObservableCollection<CLJActivityViewModel> m_Items = null;
/// <summary>
/// Gets the Items property.
/// TODO Update documentation:
/// Changes to that property's value raise the PropertyChanged event.
/// This property's value is broadcasted by the Messenger's default instance when it changes.
/// </summary>
public ObservableCollection<CLJActivityViewModel> Items
{
get
{
return m_Items;
}
set
{
if (m_Items == value)
{
return;
}
var oldValue = m_Items;
m_Items = value;
RaisePropertyChanged(ItemsPropertyName, oldValue, value, true);
}
}
#endregion
#region SelectedActivity
/// <summary>
/// The <see cref="SelectedActivity" /> property's name.
/// </summary>
public const string SelectedActivityPropertyName = "SelectedActivity";
private CLJActivityViewModel m_SelectedActivity = null;
/// <summary>
/// Gets the SelectedActivity property.
/// TODO Update documentation:
/// Changes to that property's value raise the PropertyChanged event.
/// This property's value is broadcasted by the Messenger's default instance when it changes.
/// </summary>
public CLJActivityViewModel SelectedActivity
{
get
{
return m_SelectedActivity;
}
set
{
if (m_SelectedActivity == value)
{
return;
}
var oldValue = m_SelectedActivity;
m_SelectedActivity = value;
RaisePropertyChanged(SelectedActivityPropertyName, oldValue, value, true);
}
}
#endregion
public override void Cleanup()
{
// Clean own resources if needed
base.Cleanup();
}
}
I ran into the kinda the same issue. I had to trigger the update as the user was entering text so that I could do some validation.
An easy way to achieve that is to create a custom behaviour that you can then add to any TextBox.
Mine is as follows:
public static class TextChangedBindingBehavior
{
public static readonly DependencyProperty InstanceProperty =
DependencyProperty.RegisterAttached("Instance", typeof(object), typeof(TextChangedBindingBehavior), new PropertyMetadata(OnSetInstanceCallback));
public static object GetInstance(DependencyObject obj)
{
return (object)obj.GetValue(InstanceProperty);
}
public static void SetInstance(DependencyObject obj, object value)
{
obj.SetValue(InstanceProperty, value);
}
private static void OnSetInstanceCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var textBox = d as TextBox;
if (textBox != null)
{
textBox.TextChanged -= OnTextChanged;
textBox.TextChanged += OnTextChanged;
}
}
private static void OnTextChanged(object sender, TextChangedEventArgs e)
{
var textBox = (TextBox)sender;
if(!DesignerProperties.GetIsInDesignMode(textBox))
{
textBox.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}
}
}
and you set it to the TextBox like that (Behaviors is the namespace where I put the class above):
<TextBox Behaviors:TextChangedBindingBehavior.Instance="" Text="{Binding Name, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" />
I've ran into the issue like that with TextBox, but didn't see it affecting check box. TextBox issue is happening because bound text gets updated then focus is lost. That is why if you tab first and then change your selection it works as you expect. If you change selection directly bound text doesn't get updated since focus lost message arrives too late.
One way of dealing with this issue is to force binding update every time user types text in the text box. You can make custom behaviour to keep it mvvm.
Related
I am trying to use validation to display validation errors on windows elements (actually text boxes), but I failed to get the text boxes that are not focused/edited to update their validation when conditions for failure changed (neither using INotifyDataErrorInfo nor IDataErrorInfo).
Let's say, TextBox1 validate to Error when TextBox2 holds a specific path. Now after changing the path in TextBox2, TextBox1 should clear its error automatically, but this just did not happen, I always hat to enter the TextBox and change its content for validation to update...
Therefore I intended to use Behaviors in order to bind them to a Validation Boolean value and let the behavior set the TextBox in the appropriate VisualState using the default Validation States (Valid, InvalidFocused, InvalidUnfocused).
This is my Behavior (currently only a PoC so no Dependency Property):
/// <summary>
/// Behavior for setting the visual style depending on a validation value
/// </summary>
public class TextBoxValidationBindingBehavior : BehaviorBase<TextBox>
{
/// <summary>
/// Setup the behavior
/// </summary>
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.TextChanged += this.AssociatedObject_TextChanged;
}
/// <summary>
/// Set visual state
/// </summary>
private void AssociatedObject_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox textBox = this.AssociatedObject as TextBox;
if (textBox.Text == "Test")
{
VisualStateManager.GoToState(textBox, "Valid", false);
}
else
{
if (textBox.Focus())
{
VisualStateManager.GoToState(textBox, "InvalidFocused", true);
}
else
{
VisualStateManager.GoToState(textBox, "InvalidUnfocused", true);
}
}
}
/// <summary>
/// Clean-up the behavior
/// </summary>
protected override void OnCleanup()
{
this.AssociatedObject.TextChanged -= this.AssociatedObject_TextChanged;
base.OnCleanup();
}
}
And the TextBox definition:
<TextBox Grid.Row = "0"
Grid.Column = "1"
Margin = "0, 2, 0, 2"
VerticalAlignment = "Stretch"
VerticalContentAlignment = "Center"
Text = "{Binding NewBookName}">
<b:Interaction.Behaviors>
<behavior:TextBoxValidationBindingBehavior />
</b:Interaction.Behaviors>
</TextBox>
Setting breakpoints I can see that the code gets called as expected. But the VisualStateManager.GoToState has absolutely no impact on the TextBox!
If I define a template for the text box and set custom VisualStates the behavior will work. However, the point was not to redefine visual states for the TextBox but rather to use the existing states just by associating a Behavior bound top a validation boolean and a message to display...
I'd really appreciate any hint!!! Also, I'd be happy to provide more information if required.
For the time being, I had to give up on just setting the Visual States 😣
I needed to create a Behavior that will when bound to the control add an ad-hoc ValidationRule. In principle:
For the custom validation to work (the point is only to get the default visual style on an error), the TextChanged event needs to disable the validation, update the source, then enable validation and re-update the source for the validation to happen
The IsBindingValid dependency property when changed will also update the source to trigger validation
All in all, this works:
/// <summary>
/// Behavior for setting the visual style depending on a validation value
/// </summary>
public class TextBoxValidationBindingBehavior : BehaviorBase<TextBox>
{
#region Internal Validation Class
/// <summary>
/// A validation rule that validates according to a dependency property binding rather than on control content
/// </summary>
private class BindingValidationRule : ValidationRule
{
#region Initialization
/// <summary>
/// Constructor
/// </summary>
/// <param name="that">Behavior holding this class</param>
public BindingValidationRule(TextBoxValidationBindingBehavior that) { this._that = that; }
#endregion
/// <summary>
/// Reference to behavior holding the object
/// </summary>
private readonly TextBoxValidationBindingBehavior _that;
/// <summary>
/// Flag indication that the next validation check is to be disabled / set to true
/// </summary>
public bool DisableValidationOnce = true;
/// <summary>
/// Validates the control
/// </summary>
/// <param name="value">Value to validate (ignored)</param>
/// <param name="cultureInfo">Culture Information</param>
/// <returns>Returns the <see cref="ValidationResult"/> of this validation check</returns>
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (this._that is { } that)
{
ValidationResult validationResult;
if (that.IsBindingValid || this.DisableValidationOnce)
validationResult = new ValidationResult(true, null);
else
validationResult = new ValidationResult(false, that.ErrorText);
// Re-enable validation
this.DisableValidationOnce = false;
// Set Error Tooltip
that.AssociatedObject.ToolTip = validationResult.IsValid ? null : new ToolTip() { Content = validationResult.ErrorContent };
// return result
return validationResult;
}
else throw new Exception($"Internal TextBoxValidationBindingBehavior error.");
}
}
#endregion
#region DepProp: IsBindingValid
public static readonly DependencyProperty IsBindingValidProperty = DependencyProperty.Register("IsBindingValid", typeof(bool), typeof(TextBoxValidationBindingBehavior), new PropertyMetadata(false, IsBindingValidProperty_PropertyChanged));
public bool IsBindingValid
{
get => (bool)this.GetValue(IsBindingValidProperty);
set => this.SetValue(IsBindingValidProperty, value);
}
private static void IsBindingValidProperty_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TextBoxValidationBindingBehavior _this)
{
// Avoid unnecessary notification propagation (the prop probably changed du to us updating the source property)
if (_this._isValidating) return;
// Trigger validation
if (_this.AssociatedObject is { } textBox && textBox.GetBindingExpression(TextBox.TextProperty) is { } bindingExpression)
bindingExpression.UpdateSource();
}
}
#endregion
#region DepProp: ErrorText
public static readonly DependencyProperty ErrorTextProperty = DependencyProperty.Register("ErrorText", typeof(string), typeof(TextBoxValidationBindingBehavior), new PropertyMetadata("Error"));
public string ErrorText
{
get => (string)this.GetValue(ErrorTextProperty);
set => this.SetValue(ErrorTextProperty, value);
}
#endregion
#region Private properties
/// <summary>
/// The custom validation rule to handle bound validation
/// </summary>
private BindingValidationRule _bindingValidationRule { get; set; }
/// <summary>
/// Indicate if validation already happening to avoid verbose notifications in the application
/// </summary>
private bool _isValidating;
#endregion
/// <summary>
/// Setup the behavior
/// </summary>
protected override void OnAttached()
{
base.OnAttached();
// Set handler(s)
this.AssociatedObject.TextChanged += this.AssociatedObject_TextChanged;
// Create custom validation rule
this._bindingValidationRule = new BindingValidationRule(this);
// Add rule
if (this.AssociatedObject is { } textBox && BindingOperations.GetBinding(textBox, TextBox.TextProperty) is { } binding)
{
// We must be able to handle updating the source in order to set value bypassing validation
if (binding.UpdateSourceTrigger == UpdateSourceTrigger.PropertyChanged) throw new Exception("Cannot set UpdateSourceTrigger to PropertyChanged when using TextBoxValidationBindingBehavior");
// Add custom validation rule
binding.ValidationRules.Add(this._bindingValidationRule);
}
}
/// <summary>
/// Set visual state
/// </summary>
private void AssociatedObject_TextChanged(object sender, TextChangedEventArgs e)
{
if (this.AssociatedObject is { } textBox && textBox.GetBindingExpression(TextBox.TextProperty) is { } bindingExpression)
{
this._isValidating = true;
// Remove validation before updating source (or validation will prevent source from updating if it errors)
this._bindingValidationRule.DisableValidationOnce = true;
// Update Source
bindingExpression.UpdateSource();
// Ensure we are not disabled (if UpdateSource did not call Validation)
this._bindingValidationRule.DisableValidationOnce = false;
// Trigger validation
bindingExpression.UpdateSource();
this._isValidating = false;
}
}
/// <summary>
/// Clean-up the behavior
/// </summary>
protected override void OnCleanup()
{
this.AssociatedObject.TextChanged -= this.AssociatedObject_TextChanged;
// Remove rule
if (this.AssociatedObject is { } textBox && BindingOperations.GetBinding(textBox, TextBox.TextProperty) is { } binding)
{
binding.ValidationRules.Remove(this._bindingValidationRule);
}
base.OnCleanup();
}
}
And the XAML code:
<TextBox Grid.Row = "0"
Grid.Column = "1"
Margin = "0, 2, 0, 2"
VerticalAlignment = "Stretch"
VerticalContentAlignment = "Center"
Text = "{Binding NewBookName}">
<b:Interaction.Behaviors>
<behavior:TextBoxValidationBindingBehavior IsBindingValid = "{Binding IsValidName}"
ErrorText = "Invalid name or a book with the same name already exists."/>
</b:Interaction.Behaviors>
</TextBox>
However, there are a few things I really don't like about this way of proceeding:
This procedure is very verbose in that it triggers potentially a lot of notification bindings every time it updates the source just to validate the content
It may not be thread-safe?!
While it is theoretically possible to use it with other validation rules, it would probably need a lot of code to get it to work
I find this quite hacky...
I hope this can help others or if one has a better idea: YOU ARE WELCOME!!! 😊
Can anyone show me a simple working example for a WPF MVVM application to set the ItemsSource of combobox B based on the SelectedItem of ComboBox A?
It seems from what I've found on this site that it gets all too complicated all too quickly.
What's the "right" MVVM way to get it done?
Thank you.
EDIT
I updated using Didier's example.
An extract of my XAML:
<ComboBox Name="BrowserStackDesktopOS" ItemsSource="Binding Platforms.AvailableBrowserStackDesktopOSes}" SelectedIndex="0" SelectedItem="{Binding Platforms.BrowserStackDesktopOSSelectedValue, Mode=TwoWay}"/>
<ComboBox Name="BrowserStackDesktopOSVersion" ItemsSource="{Binding Platforms.AvailableBrowserStackDesktopOSVersions}" SelectedIndex="0" SelectedItem="{Binding Platforms.BrowserStackDesktopOSVersionSelectedValue, Mode=TwoWay}"/>
<ComboBox Name="BrowserStackDesktopBrowser" ItemsSource="{Binding Platforms.AvailableBrowserStackDesktopBrowsers}" SelectedIndex="0" SelectedItem="{Binding Platforms.BrowserStackDesktopBrowserSelectedValue, Mode=TwoWay}"/>
<ComboBox Name="BrowserStackDesktopBrowserVersion" ItemsSource="{Binding Platforms.AvailableBrowserStackDesktopBrowserVersions}" SelectedIndex="0" SelectedItem="{Binding Platforms.BrowserStackDesktopBrowserVersionSelectedValue, Mode=TwoWay}"/>
And an example of my code behind:
public string BrowserStackDesktopOSSelectedValue {
get { return (string)GetValue(BrowserStackDesktopOSSelectedValueProperty); }
set { SetValue(BrowserStackDesktopOSSelectedValueProperty, value);
AvailableBrowserStackDesktopOSVersions = AvailableBrowserStackDesktopPlatforms.GetOSVersions(BrowserStackDesktopOSSelectedValue);
NotifyPropertyChanged("BrowserStackDesktopOSSelectedValue");
}
}
However when I select a value for the first ComboBox nothing happens. I am wanting the Itemsource of the next ComboBox to by populated.
What have I done wrong?
Basically you need to expose in your MVVM 2 collections of values for combo-box choices and two properties for selected values.
In the beginning only the first collection if filled with values. When the first selected value changes the second collection will be filled in with appropriate values. Here is an example implementation:
Code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
//Set the data context of the window
DataContext = new TestVM();
}
}
public class TestVM : INotifyPropertyChanged
{
#region Class attributes
protected static string[] firstComboValues = new string[] { "Choice_1", "Choice_2" };
protected static string[][] secondComboValues =
new string[][] {
new string[] { "value_1_1", "value_1_2", "value_1_3" },
new string[] { "value_2_1", "value_2_2", "value_2_3" }
};
#endregion
#region Public Properties
#region FirstSelectedValue
protected string m_FirstSelectedValue;
/// <summary>
///
/// </summary>
public string FirstSelectedValue
{
get { return m_FirstSelectedValue; }
set
{
if (m_FirstSelectedValue != value)
{
m_FirstSelectedValue = value;
UpdateSecondComboValues();
NotifyPropertyChanged("FirstSelectedValue");
}
}
}
#endregion
#region SecondSelectedValue
protected string m_SecondSelectedValue;
/// <summary>
///
/// </summary>
public string SecondSelectedValue
{
get { return m_SecondSelectedValue; }
set
{
if (m_SecondSelectedValue != value)
{
m_SecondSelectedValue = value;
NotifyPropertyChanged("SecondSelectedValue");
}
}
}
#endregion
#region FirstComboValues
protected ObservableCollection<string> m_FirstComboValues;
/// <summary>
///
/// </summary>
public ObservableCollection<string> FirstComboValues
{
get { return m_FirstComboValues; }
set
{
if (m_FirstComboValues != value)
{
m_FirstComboValues = value;
NotifyPropertyChanged("FirstComboValues");
}
}
}
#endregion
#region SecondComboValues
protected ObservableCollection<string> m_SecondComboValues;
/// <summary>
///
/// </summary>
public ObservableCollection<string> SecondComboValues
{
get { return m_SecondComboValues; }
set
{
if (m_SecondComboValues != value)
{
m_SecondComboValues = value;
NotifyPropertyChanged("SecondComboValues");
}
}
}
#endregion
#endregion
public TestVM()
{
FirstComboValues = new ObservableCollection<string>(firstComboValues);
}
/// <summary>
/// Update the collection of values for the second combo box
/// </summary>
protected void UpdateSecondComboValues()
{
int firstComboChoice;
for (firstComboChoice = 0; firstComboChoice < firstComboValues.Length; firstComboChoice++)
{
if (firstComboValues[firstComboChoice] == FirstSelectedValue)
break;
}
if (firstComboChoice == firstComboValues.Length)// just in case of a bug
SecondComboValues = null;
else
SecondComboValues = new ObservableCollection<string>(secondComboValues[firstComboChoice]);
}
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
And the associated XAML
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="window" x:Class="Testing1.MainWindow">
<Grid>
<Grid HorizontalAlignment="Center" VerticalAlignment="Center" Width=" 300">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="10"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ComboBox x:Name="FirstOne" ItemsSource="{Binding FirstComboValues}" SelectedItem="{Binding FirstSelectedValue, Mode=TwoWay}"/>
<ComboBox x:Name="SecondOne" ItemsSource="{Binding SecondComboValues}" SelectedItem="{Binding SecondSelectedValue, Mode=TwoWay}" Grid.Row="2"/>
</Grid>
</Grid>
</Window>
As you can see the SelectedValue properties of combo boxes are binded in TwoWay mode so when SelectedValue property of the combo box changes it changes the value on the VM side. And in FirstSelectedValue property setter UpdateSecondComboValues() method is called to update values for the second combo box.
EDIT:
It happens because you mixed both INotifPropertyChanged and DependencyObject. You should choose one of them. Usually you implement INotifyPropertyChanged in your VM and the code in the property setter will work.
If you inherit from DependencyObject however, you should not write any code in the setter/getter. It will never be called by the TwoWay binding. It will just call GetValue(...) internally. To be able to execute an action on DependencyProperty change you should declare it differently with a property changed handler:
#region BrowserStackDesktopOSSelectedValue
/// <summary>
/// BrowserStackDesktopOSSelectedValue Dependency Property
/// </summary>
public static readonly DependencyProperty BrowserStackDesktopOSSelectedValue Property =
DependencyProperty.Register("BrowserStackDesktopOSSelectedValue ", typeof(string), typeof(YourVM),
new FrameworkPropertyMetadata((string)null,
new PropertyChangedCallback(OnBrowserStackDesktopOSSelectedValue Changed)));
/// <summary>
/// Gets or sets the BrowserStackDesktopOSSelectedValue property. This dependency property
/// indicates ....
/// </summary>
public string BrowserStackDesktopOSSelectedValue
{
get { return (string)GetValue(BrowserStackDesktopOSSelectedValue Property); }
set { SetValue(BrowserStackDesktopOSSelectedValue Property, value); }
}
/// <summary>
/// Handles changes to the BrowserStackDesktopOSSelectedValue property.
/// </summary>
private static void OnBrowserStackDesktopOSSelectedValue Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
YourVM target = (YourVM)d;
string oldBrowserStackDesktopOSSelectedValue = (string)e.OldValue;
string newBrowserStackDesktopOSSelectedValue = target.BrowserStackDesktopOSSelectedValue ;
target.OnBrowserStackDesktopOSSelectedValue Changed(oldBrowserStackDesktopOSSelectedValue , newBrowserStackDesktopOSSelectedValue );
}
/// <summary>
/// Provides derived classes an opportunity to handle changes to the BrowserStackDesktopOSSelectedValue property.
/// </summary>
protected virtual void OnBrowserStackDesktopOSSelectedValue Changed(string oldBrowserStackDesktopOSSelectedValue , string newBrowserStackDesktopOSSelectedValue )
{
//Here write some code to update your second ComboBox content.
AvailableBrowserStackDesktopOSVersions = AvailableBrowserStackDesktopPlatforms.GetOSVersions(BrowserStackDesktopOSSelectedValue);
}
#endregion
By the way I always use Dr WPF snippets to write DPs so it goes much faster.
I used this tutorial to build a custom control. Now, I'd like to add a simple message (a textblock) to the user control to give the user some guidance. I think I can add a public property, like FileName in the tutorial, but how do I wire up the textblock's Text property to the property in the code behind? And then make sure the textblock message updates if the property changes.
I like the idea of being able to set the message in code, via a property, because I will likely have multiple controls of this custom control type on a page. I'm just a bit stumped on wiring it up.
Thanks!
This would be your code behind, which implements INotifyPropertyChanged:
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _fileName;
/// <summary>
/// Get/Set the FileName property. Raises property changed event.
/// </summary>
public string FileName
{
get { return _fileName; }
set
{
if (_fileName != value)
{
_fileName = value;
RaisePropertyChanged("FileName");
}
}
}
public MainWindow()
{
DataContext = this;
FileName = "Testing.txt";
}
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
This would be your XAML that binds to the property:
<TextBlock Text="{Binding FileName}" />
EDIT:
Added DataContext = this; i don't normally bind to the code behind (I use MVVM).
I am using WPF Ribbon 4. I have a RibbonSplitButton control with dropdown menu of menu items.
When I set IsEnabled property of RibbonSplitButton to false only top button becomes disabled, not the button which opens dropdown menu.
Thanks in advance.
I solved this problem by creating my own split button, inheriting from RibbonSplitButton and adding an dependency property that I can bind to for enabling or disabling the split button alone.
public class MyRibbonSplitButton : RibbonSplitButton
{
public MyRibbonSplitButton()
: base()
{
}
/// <summary>
/// Gets or sets a value indicating whether the toggle button is enabled.
/// </summary>
/// <value><c>true</c> if the toggle button should be enabled; otherwise, <c>false</c>.</value>
public bool IsToggleButtonEnabled
{
get { return (bool)GetValue(IsToggleButtonEnabledProperty); }
set { SetValue(IsToggleButtonEnabledProperty, value); }
}
/// <summary>
/// Identifies the <see cref="IsToggleButtonEnabled"/> dependency property
/// </summary>
public static readonly DependencyProperty IsToggleButtonEnabledProperty =
DependencyProperty.Register(
"IsToggleButtonEnabled",
typeof(bool),
typeof(MyRibbonSplitButton),
new UIPropertyMetadata(true, new PropertyChangedCallback(MyRibbonSplitButton.ToggleButton_OnIsEnabledChanged)));
/// <summary>
/// Handles the PropertyChanged event for the IsToggleButtonEnabledProperty dependency property
/// </summary>
private static void ToggleButton_OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var button = sender as MyRibbonSplitButton;
var toggleButton = button.GetTemplateChild("PART_ToggleButton") as RibbonToggleButton;
toggleButton.IsEnabled = (bool)e.NewValue;
}
}
and in XAML:
<local:MyRibbonSplitButton Label="New" Command="{Binding SomeCommand}"
LargeImageSource="Images/Large/New.png"
ItemsSource="{Binding Templates}"
IsToggleButtonEnabled="{Binding HasTemplates}"/>
You can simply add a DropDownOpened="RibbonMenuButton_OnDropDownOpened" to the WPF and then
private void RibbonMenuButton_OnDropDownOpened(object sender, EventArgs e)
{
var rsb = sender as RibbonSplitButton;
if (rsb == null) return;
if (DataContext is GameCardViewModel vm)
{
rsb.IsDropDownOpen = vm.EasyInputMode;
}
}
Using silverlight, I have a listbox with ItemsSource bound to an ObservableCollection which is updated asynchronously. I would like to automatically select the first item in the listbox as soon as the binding is finished updating.
I can't find a good way to make this happen. I don't see any useful events to handle on the listbox and if I bind to the collection's CollectionChanged event, the binding hasn't updated yet so if I set the listbox.selectedindex at that point I get an exception that the value is out of range. Any ideas? Maybe some way to hook the binding update?
I spent a long time searching the internet to the solution for this problem, and basically ended up stumbling on the solution.
What you want to do is bind your listbox to an ICollectionView. Then make sure that you DON'T have IsSynchronizedWithCurrentItem set to false.
Bad, won't work
IsSynchronizedWithCurrentItem="False"
This is the Silverlight default, don't waste your time typing it out
IsSynchronizedWithCurrentItem="{x:Null}"
This will throw an error at runtime, and I believe is the same as {x:null} anyways
IsSynchronizedWithCurrentItem="True"
ICollectionView has a method called MoveCurrentToFirst. That name seems a bit ambiguous, but it does actually move the CurrentItem pointer to the first item (I initally thought it reordered the collection by moving whatever item you had selected to the first item position). The IsSynchronizedWithCurrentItem property allows Listbox (or any control implementing Selector) to work magic with ICollectionViews. In your code you can call ICollectioView.CurrentItem instead of whatever you bound Listbox.SelectedItem to get the currently selected item.
Here is how I make my ICollectionView available to my view (I'm using MVVM) :
public System.ComponentModel.ICollectionView NonModifierPricesView
{
get
{
if (_NonModifierPricesView == null)
{
_NonModifierPricesView = AutoRefreshCollectionViewSourceFactory.Create(x => ((MenuItemPrice)x).PriceType == MenuItemPrice.PriceTypes.NonModifier);
_NonModifierPricesView.Source = Prices;
_NonModifierPricesView.ApplyFilter(x => ((MenuItemPrice)x).DTO.Active == true);
}
ICollectionView v = _NonModifierPricesView.View;
v.MoveCurrentToFirst();
return v;
}
}
Now, as you want to bind to an observable collection, you cannot use the default CollectionViewSource as it is not aware of updates to the source collection. You probably noticed that I am using a custom CVS implementation called AutoRefreshCollectionViewSource. If memory serves, I found the code online and modified it for my own uses. I've added extra functionality for filtering, so could probably clean these classes up even more.
Here is my version of the code :
AutoRefreshCollectionViewSource.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows.Data;
public class AutoRefreshCollectionViewSource : System.Windows.Data.CollectionViewSource
{
// A delegate for launching a refresh of the view at a different priority.
private delegate void NoArgDelegate();
private Predicate<object> MyFilter; // this is the filter we put on when we do ApplyFilter
private Predicate<object> BaseFilter; // this is the filter that is applied always
public AutoRefreshCollectionViewSource(Predicate<object> _baseFilter) : base()
{
BaseFilter = _baseFilter;
if (BaseFilter == null)
BaseFilter = x => true;
}
/// <summary>
/// A collection containing all objects whose event handlers have been
/// subscribed to.
/// </summary>
private List<INotifyPropertyChanged> colSubscribedItems = new List<INotifyPropertyChanged>();
// We must override the OnSourceChanged event so that we can subscribe
// to the objects in the new collection (and unsubscribe from the old items).
protected override void OnSourceChanged(object oldSource, object newSource)
{
// Unsubscribe from the old source.
if (oldSource != null)
SubscribeSourceEvents(oldSource, true);
// Subscribe to the new source.
if (newSource != null)
SubscribeSourceEvents(newSource, false);
base.OnSourceChanged(oldSource, newSource);
}
/// <summary>
/// Adds or Removes EventHandlers to each item in the source collection as well as the
/// collection itself (if supported).
/// </summary>
/// <param name="source">The collection to (un)subscribe to and whose objects should be (un)subscribed.</param>
/// <param name="remove">Whether or not to subscribe or unsubscribe.</param>
private void SubscribeSourceEvents(object source, bool remove)
{
// Make sure the source is not nothing.
// This may occur when setting up or tearing down this object.
if (source != null)
if (source is INotifyCollectionChanged)
// We are (un)subscribing to a specialized collection, it supports the INotifyCollectionChanged event.
// (Un)subscribe to the event.
if (remove)
((INotifyCollectionChanged)source).CollectionChanged -= Handle_INotifyCollectionChanged;
else
((INotifyCollectionChanged)source).CollectionChanged += Handle_INotifyCollectionChanged;
if (remove)
// We are unsubscribing so unsubscribe from each object in the collection.
UnsubscribeAllItemEvents();
else
// We are subscribing so subscribe to each object in the collection.
SubscribeItemsEvents((IEnumerable)source, false);
}
/// <summary>
/// Unsubscribes the NotifyPropertyChanged events from all objects
/// that have been subscribed to.
/// </summary>
private void UnsubscribeAllItemEvents()
{
while (colSubscribedItems.Count > 0)
SubscribeItemEvents(colSubscribedItems[0], true);
}
/// <summary>
/// Subscribes or unsubscribes to the NotifyPropertyChanged event of all items
/// in the supplied IEnumerable.
/// </summary>
/// <param name="items">The IEnumerable containing the items to (un)subscribe to/from.</param>
/// <param name="remove">Whether or not to subscribe or unsubscribe.</param>
private void SubscribeItemsEvents(IEnumerable items, bool remove)
{
foreach (object item in items)
SubscribeItemEvents(item, remove);
}
/// <summary>
/// Subscribes or unsubscribes to the NotifyPropertyChanged event if the supplied
/// object supports it.
/// </summary>
/// <param name="item">The object to (un)subscribe to/from.</param>
/// <param name="remove">Whether or not to subscribe or unsubscribe.</param>
private void SubscribeItemEvents(object item, bool remove)
{
if (item is INotifyPropertyChanged)
// We only subscribe of the object supports INotifyPropertyChanged.
if (remove)
{
// Unsubscribe.
((INotifyPropertyChanged)item).PropertyChanged -= Item_PropertyChanged;
colSubscribedItems.Remove((INotifyPropertyChanged)item);
}
else
{
// Subscribe.
((INotifyPropertyChanged)item).PropertyChanged += Item_PropertyChanged;
colSubscribedItems.Add((INotifyPropertyChanged)item);
}
}
/// <summary>
/// Handles a property changed event from an item that supports INotifyPropertyChanged.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="e">The event arguments associated with the event.</param>
/// <remarks></remarks>
private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// By default, we do not need to refresh.
bool refresh = false;
if (e.PropertyName == "Active" || e.PropertyName == "DTO.Active")
refresh = true;
if (refresh)
// Call the refresh.
// Notice that the dispatcher will make the call to Refresh the view. If the dispatcher is not used,
// there is a possibility for a StackOverFlow to result.
this.Dispatcher.BeginInvoke(new NoArgDelegate(this.View.Refresh), null);
}
/// <summary>
/// Handles the INotifyCollectionChanged event if the subscribed source supports it.
/// </summary>
private void Handle_INotifyCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
SubscribeItemsEvents(e.NewItems, false);
break;
case NotifyCollectionChangedAction.Remove:
SubscribeItemsEvents(e.OldItems, true);
break;
case NotifyCollectionChangedAction.Replace:
SubscribeItemsEvents(e.OldItems, true);
SubscribeItemsEvents(e.NewItems, false);
break;
case NotifyCollectionChangedAction.Reset:
UnsubscribeAllItemEvents();
SubscribeItemsEvents((IEnumerable)sender, false);
break;
}
}
public void ApplyFilter(Predicate<object> f)
{
if (f != null)
MyFilter = f;
this.View.Filter = x => MyFilter(x) && BaseFilter(x);
this.View.Refresh();
}
public void RemoveFilter()
{
this.View.Filter = BaseFilter;
this.View.Refresh();
}
}
AutoRefreshCollectionViewSourceFactory.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
public class AutoRefreshCollectionViewSourceFactory
{
private static List<AutoRefreshCollectionViewSource> Collections;
public static AutoRefreshCollectionViewSource Create()
{
if (Collections == null)
Collections = new List<AutoRefreshCollectionViewSource>();
AutoRefreshCollectionViewSource cvs = new AutoRefreshCollectionViewSource(null);
Collections.Add(cvs);
return cvs;
}
public static AutoRefreshCollectionViewSource Create(Predicate<object> p)
{
if (Collections == null)
Collections = new List<AutoRefreshCollectionViewSource>();
AutoRefreshCollectionViewSource cvs = new AutoRefreshCollectionViewSource(p);
Collections.Add(cvs);
return cvs;
}
public static void ApplyFilterOnCollections()
{
foreach (AutoRefreshCollectionViewSource cvs in Collections)
cvs.ApplyFilter(null);
}
public static void RemoveFilterFromCollections()
{
foreach (AutoRefreshCollectionViewSource cvs in Collections)
cvs.RemoveFilter();
}
public static void CleanUp()
{
Collections = null;
}
}
Another thing you can do is create a UserControl which inherits from ListBox, expose a ItemsChanged by overriding the OnItemsChanged method, and handle this event and set the ListBox's SelectedIndex to 0.
public partial class MyListBox : ListBox
{
public delegate void ItemsSourceChangedHandler(object sender, EventArgs e);
#region Override
protected override void OnItemsChanged(
NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
OnItemsChangedEvent(e);
}
#endregion Override
#region Class Events
public delegate void ItemsChangedEventHandler(object sender,
NotifyCollectionChangedEventArgs e);
public event ItemsChangedEventHandler ItemsChanged;
private void OnItemsChangedEvent(
NotifyCollectionChangedEventArgs e)
{
if (ItemsChanged != null)
{
ItemsChanged(this, e);
}
}
#endregion Class Events
}
XAML for the User Control:
<ListBox x:Class="CoverArtRefiner.MyListBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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 x:Name="LayoutRoot" Background="White">
</Grid>
</ListBox>
On your screen add the following:
xmlns:local="clr-namespace:<The Namespace which MyListBox is contained in>"
Now to add the custom control onto your window/usercontrol:
<local:MyListBox ItemsChanged="listBox_ItemsChanged" Background="Black" />
And finally to handle the event:
private void listBox_ItemsChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
MyListBox listBox = (MyListBox)sender;
if (listBox.Items.Count > 0)
listBox.SelectedIndex = 0;
}
On the ListBox, bind the SelectedItem property to a property on your codebind or viewmodel. Then, in the async callback handler set the property to the first item in the collection and raise the PropertyChanged event for the property(unless you already raise the event in the setter of your property):
MySelectedListItem = _entitylist.FirstOrDefault();
RasisePropertyChanged("MySelectedListItem");