Update ThemeResources from c#? - c#

I've got a void that analyses an image, extracts two dominant colors, and replaces "SystemControlForegroundAccentBrush" and "SystemControlHighlightAccentBrush", that I'm overriding in App.xaml. It works well, except that it takes a bit of time to analyse the image, so it changes the colors once all the controls in my page have already loaded.
As a result, they stay the old accent color. How can I get them to bind dynamically to that ThemeRessource?
Here's my app.xaml:
<Application.Resources>
<ResourceDictionary x:Name="resdic">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="SystemControlForegroundAccentBrush"/>
<SolidColorBrush x:Key="SystemControlHighlightAccentBrush"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Application.Resources>
This is the (very simplified) part of the ColorExtractor.cs class that changes the colors:
public async static void Analyse()
{
Application.Current.Resources["SystemControlForegroundAccentBrush"] = new SolidColorBrush(color);
Application.Current.Resources["SystemControlHighlightAccentBrush"] = new SolidColorBrush(color2);
}
And I've got a bunch of controls in Page.xaml that have their Foreground set as such:
<TextBlock Foreground="{ThemeResource SystemControlForegroundAccentBrush]"/>
I call ColorExtractor.Analyse() in my Page.xaml.cs (at the OnNavigatedTo event). I can always create an event that gets fired once the colors are set, but I need to find a way to update the colors of all the controls in my page once that's done.

You can have a look at how Template 10 does theming changes, but in their case they are defining two different themes in resource dictionaries in advance. You can find their code in the repo on Github, but here are some of the code used:
(Window.Current.Content as FrameworkElement).RequestedTheme = value.ToElementTheme();
Views.Shell.SetRequestedTheme(value, UseBackgroundChecked);
From here
public static void SetRequestedTheme(ApplicationTheme theme, bool UseBackgroundChecked)
{
WindowWrapper.Current().Dispatcher.Dispatch(() =>
{
(Window.Current.Content as FrameworkElement).RequestedTheme = theme.ToElementTheme();
ParseStyleforThemes(theme);
HamburgerMenu.NavButtonCheckedForeground = NavButtonCheckedForegroundBrush;
HamburgerMenu.NavButtonCheckedBackground = (UseBackgroundChecked) ?
NavButtonCheckedBackgroundBrush : NavButtonBackgroundBrush;
HamburgerMenu.NavButtonCheckedIndicatorBrush = (UseBackgroundChecked) ?
Colors.Transparent.ToSolidColorBrush() : NavButtonCheckedIndicatorBrush;
HamburgerMenu.SecondarySeparator = SecondarySeparatorBrush;
List<HamburgerButtonInfo> NavButtons = HamburgerMenu.PrimaryButtons.ToList();
NavButtons.InsertRange(NavButtons.Count, HamburgerMenu.SecondaryButtons.ToList());
List<HamburgerMenu.InfoElement> LoadedNavButtons = new List<HamburgerMenu.InfoElement>();
foreach (var hbi in NavButtons)
{
StackPanel sp = hbi.Content as StackPanel;
if (hbi.ButtonType == HamburgerButtonInfo.ButtonTypes.Literal) continue;
ToggleButton tBtn = sp.Parent as ToggleButton;
Button btn = sp.Parent as Button;
if (tBtn != null)
{
var button = new HamburgerMenu.InfoElement(tBtn);
LoadedNavButtons.Add(button);
}
else if (btn != null)
{
var button = new HamburgerMenu.InfoElement(btn);
LoadedNavButtons.Add(button);
continue;
}
else
{
continue;
}
Rectangle indicator = tBtn.FirstChild<Rectangle>();
indicator.Visibility = ((!hbi.IsChecked ?? false)) ? Visibility.Collapsed : Visibility.Visible;
if (!hbi.IsChecked ?? false) continue;
ContentPresenter cp = tBtn.FirstAncestor<ContentPresenter>();
cp.Background = NavButtonCheckedBackgroundBrush;
cp.Foreground = NavButtonCheckedForegroundBrush;
}
LoadedNavButtons.ForEach(x => x.RefreshVisualState());
});
}
From here

I've got a void that analyses an image, extracts two dominant colors, and replaces "SystemControlForegroundAccentBrush" and "SystemControlHighlightAccentBrush", that I'm overriding in App.xaml.
First I don't think your code in app.xaml can override the ThemeResource, and you used this Brush in the Page like this:
<TextBlock Foreground="{ThemeResource SystemControlForegroundAccentBrush}" Text="Hello World!" FontSize="50" />
If you press "F12" on the SystemControlForegroundAccentBrush, you can find this resource actually in the "generic.xaml" file.
Now suppose your ColorExtractor.cs class works fine and ColorExtractor.Analyse() can override the color of those two brushes, and you have many controls in the page uses these two resources, refreshing the Page here can solve your problem.
But I think it's better that not put this operation in OnNavagateTo event or Page.Loaded event, there is no refresh method in UWP, we use navigating again to this page to refresh, so if putting this operation in OnNavagateTo event or Page.Loaded event, every time you navigate to this page, the resources will be overrided and it will navigate again. So I put this operation in a Button click event like this:
public bool Reload()
{
return Reload(null);
}
private bool Reload(object param)
{
Type type = this.Frame.CurrentSourcePageType;
if (this.Frame.BackStack.Any())
{
type = this.Frame.BackStack.Last().SourcePageType;
param = this.Frame.BackStack.Last().Parameter;
}
try { return this.Frame.Navigate(type, param); }
finally
{
this.Frame.BackStack.Remove(this.Frame.BackStack.Last());
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
ColorExtractor.Analyse();
Reload();
}

In the end, I decided to create an event in ColorExtractor.cs :
public static event EventHandler Analysed;
public async static void Analyse(BitmapImage poster)
{
//Analyse colors
Analysed(null, null);
}
And then, on my MainPage.xaml.cs:
ColorExtractor.Analyse(bmp);
ColorExtractor.Analysed += (sender, EventArgs) =>
{
//Set Page foreground color, as a lot of controls are dynamically binded to their parent's foreground brush.
//If a control isn't automatically binded, all I have to do is say: Foreground="{Binding Foreground, ElementName=page}"
page.Foreground = Application.Current.Resources["SystemControlForegroundAccentBrush"] as SolidColorBrush;
page.BorderBrush = Application.Current.Resources["SystemControlHighlightAccentBrush"] as SolidColorBrush;
//Reload any custom user control that sets it's children's color when it's loaded.
backdrop.UserControl_Loaded(null, null);
};
So I'm not actually binding my controls to the ForegroundAccentBrush directly, but this works without needing to re-navigate to the page.

Related

How to change SystemAccentColor at runtime (UWP)?

The app has a GridView in which each item is a color that the user can choose to customize the UI overriding the default SystemAccentColor (the one is defined by user on their system).
I managed to get the color of the item but even though I assign it as new value for SystemAccentColor I am not able to update the UI.
private void GridView_ItemClick(object sender, ItemClickEventArgs e)
{
// FIRST APROACH -----
GridViewItem gridViewItem = GVColors.ContainerFromItem(e.ClickedItem) as GridViewItem;
Ellipse ellipseItem = gridViewItem.FindDescendant<Ellipse>();
var theColor = (SolidColorBrush)ellipseItem.Fill;
Application.Current.Resources["SystemAccentColor"] = theColor;
// SECOND APPROACH ----
Windows.UI.Color theColor2 = new Windows.UI.Color
{
A = 1,
R = 176,
G = 37,
B = 37
};
var root = (FrameworkElement)Window.Current.Content;
root.Resources["SystemAccentColor"] = theColor2;
}
I'm currently reading this blog entry XAML Brewer, by Diederik Krols: Using a Dynamic System Accent Color in UWP but I want to know if the community knows another approach to change the accent color at runtime (or a method that I'm not aware of to Update/refresh the UI).
I assign it as new value for SystemAccentColor I am not able to update the UI.
Since you statically bind SystemAccentColor and it doesn't implement INotifyPropertyChanged interface, event though the value of SystemAccentColor changes, the UI which bound with it won't update directly.
Based on your requirement, you can add a class which implements INotifyPropertyChanged interface and add the SystemAccentColor as property in it. Then init the class instance in Application.Resources. After that, bind the UI with the SystemAccentColor property. For example, I create a class named SystemAccentColorSetting.
SystemAccentColorSetting.cs:
public class SystemAccentColorSetting : INotifyPropertyChanged
{
private SolidColorBrush systemAccentColor = new SolidColorBrush(Colors.Red);
public SolidColorBrush SystemAccentColor
{
get {
return systemAccentColor;
}
set {
systemAccentColor = value; OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
App.xaml:
<Application.Resources>
<ResourceDictionary>
<local:SystemAccentColorSetting x:Key="SystemAccentColorSetting"/>
</ResourceDictionary>
</Application.Resources>
Usage:
Assume that we bind the Background of Button with the SystemAccentColor property.
.xaml:
<Button x:Name="MyButton" Background="{Binding SystemAccentColor, Source={StaticResource SystemAccentColorSetting}}">hello</Button>
.cs:
If you want to change the value of Background, just change the SystemAccentColor property.
private void GridView_ItemClick(object sender, ItemClickEventArgs e)
{
GridViewItem gridViewItem = GVColors.ContainerFromItem(e.ClickedItem) as GridViewItem;
Ellipse ellipseItem = gridViewItem.FindDescendant<Ellipse>();
var theColor = (SolidColorBrush)ellipseItem.Fill;
((SystemAccentColorSetting)Application.Current.Resources["SystemAccentColorSetting"]).SystemAccentColor = theColor;
}
Starting from Win 10 1809 (build 17763), you can use the ColorPaletteResources Class.
By using it, you can change not only the Accent color at runtime, but also all the other system default colors for UI controls.
Due to a bug, you forst have to declare that ColorPaletteResources object into your App's XAML resources (with a key) and then you can use it at runtime.
Another bug is that, from what I have experimented some time ago, you can only change the Accent color for now, so you are lucky.
To see it in action, you can download Fluent XAML Theme Editor from the Windows Store or from GitHub.
Here is the link to the class itself and to some guidelines on how to use it.

UWP/C# Applying selected Colour to NavigationView

Finally managed to get the Colorpickers down and sorted and they currently work on a OnNavigateTo basis.
When I pick a color from color picker I would like to to be applied eith instantly to the Foreground of my NavigationViewItems OR apply once i click the button within the settings page called TextColourApply_Click.
The mentioned color picker is on the settingspage currently and the NavigationViewItems are on the MainPage.
I was looking at doing a UI refresh but this doesnt work with UWP as far as i can tell. As a work around, i was looking at doing a current Frame navigate but this doesnt work
I have the following that allows the selected colour to apply when navigating back to the "MainPage":
protected override void OnNavigatedTo(NavigationEventArgs e)
{
SolidColorBrush DefaultTextColour = Application.Current.Resources["DefaultTextColour"] as SolidColorBrush;
if (ColourSelections.TextColour != null)
{
DefaultTextColour = ColourSelections.TextColour;
}
foreach (var item in NavView.MenuItems.OfType<NavigationViewItem>())
{
item.Foreground = DefaultTextColour;
}
}
Any ideas on how to impliment this would be appreciated. Thank you
if your desired behaviour is following :
When I pick a color from color picker it should be applied instantly to the Foreground of my NavigationViewItems and color picker is on settings page.
In that case you do not need OnNavigatedTo on your MainPage and you do not need the Apply as well, So remove your OnNavigatedTo method also remove the Apply button from your settings page and then Just do the following:
Create a public static property within your ShellPage (the page where your NavigationView exists) that will expose your NavigationView, and make sure to initialize it within the constructor of your ShellPage.
public static NavigationView MyNavView;
public ShellPage()
{
this.InitializeComponent();
MyNavView = NavView; //here you assign your navigation view to the public static property so you can access it outside this shell page as well.
}
Now within the colorChanged event (in your settings page) of your color picker assign the color to the foreground of your navigationmenuItems.
private void TextColourPicker_ColorChanged(ColorPicker sender, ColorChangedEventArgs args)
{
SolidColorBrush DefaultTextColour = new SolidColorBrush(TextColourPicker.Color);
foreach (var item in ShellPage.MyNavView.MenuItems.OfType<NavigationViewItem>())
{
item.Foreground = DefaultTextColour;
}
}
and just to make sure that whenever your app is loaded for the first time you get the default color set in your resources, assign a Loaded event to your NavigationView and set the default color in there.
add the loaded event in xaml like this :
<NavigationView x:Name="NavView" Loaded="NavView_Loaded">
and the event in your backend will be :
private void NavView_Loaded(object sender, object args)
{
SolidColorBrush DefaultTextColour = Application.Current.Resources["DefaultTextColour"] as SolidColorBrush;
foreach (var item in NavView.MenuItems.OfType<NavigationViewItem>())
{
item.Foreground = DefaultTextColour;
}
}
Please note that now you do not even need the public static class you were using before for saving the colors, so you can remove that class as well.

How do I programmatically add event handlers to RadioButtons in a DataGridTemplateColumn that is dynamically created using XamlReader?

I am trying to add event handlers to RadioButtons in a DataGridTemplateColumn which is created through XamlReader.
Here is the code I'm using:
string templateColumnStart = #"<DataGridTemplateColumn Header='Svar' Width='200' x:Name='myTemplateColumn'
xmlns ='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation='Horizontal'>";
string templateColumnEnd = #"</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>";
string radioButtonString = #"<RadioButton Name='rb1' Content='1' IsChecked='false'/>";
string fullXamlString = templateColumnStart + radioButtonString + templateColumnEnd;
MemoryStream stream = new MemoryStream(ASCIIEncoding.UTF8.GetBytes(fullXamlString));
DataGridTemplateColumn templateColumn = (DataGridTemplateColumn)XamlReader.Load(stream);
StackPanel template = (StackPanel)templateColumn.CellTemplate.LoadContent();
RadioButton radiobutton = (RadioButton)template.FindName("rb1");
radiobutton.Checked += new RoutedEventHandler(rb_Checked);
myDataGrid.Columns.Add(templateColumn);
The reason I'm using XamlReader to create the TemplateColumn is that the number of RadioButtons needed in the column will vary, and as such, I need the ability to dynamically change the number of RadioButtons created.
The column is created and added to the DataGrid without a problem, and the RadioButton is displayed correctly. However, the event handler does not seem to have been added, as checking the button does not trigger it.
As a side note, simply adding "Checked='rb_Checked'" to the radioButtonString throws a XamlParserException, as XamlReader is not able to handle event handlers.
Any help with this would be greatly appreciated.
The main issue here is that you are hooking up the event handler to the wrong RadioButton instance. A new one will be created by the WPF runtime when the CellTemplate is applied so calling the LoadContent() method is meaningless here.
Unfortunately the DataGridColumn class doesn't raise any events when the GenerateElement method is invoked so you will have to create your own custom DataGridColumn to be able to get a reference to the StackPanel once the template has actually been applied. Before this there is no StackPanel nor RadioButton. A DataTemplate is as the name implies just a template that is being applied eventually.
Here is what you could do if you are not satisfied with using an implicit Style that gets applied to all RadioButtons.
Create a custom class that inherits from DataGridTemplateColumn and raises an event when the GenerateElement method is called:
namespace WpfApplication2
{
public class CellElementGeneratedEventArgs : EventArgs
{
public FrameworkElement Content { get; set; }
}
public delegate void CellElementGeneratedEventHandler(object sender, CellElementGeneratedEventArgs e);
public class MyDataGridTemplateColumn : DataGridTemplateColumn
{
public event CellElementGeneratedEventHandler ElementGenerated;
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
FrameworkElement fe = base.GenerateElement(cell, dataItem);
if(fe != null)
fe.Loaded += Fe_Loaded;
return fe;
}
private void Fe_Loaded(object sender, RoutedEventArgs e)
{
if (ElementGenerated != null)
ElementGenerated(this, new CellElementGeneratedEventArgs() { Content = sender as FrameworkElement });
}
}
}
And modify your XAML markup and code slightly:
string templateColumnStart = #"<local:MyDataGridTemplateColumn Header='Svar' Width='200' x:Name='myTemplateColumn' xmlns ='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:local='clr-namespace:WpfApplication2;assembly=WpfApplication2'> <DataGridTemplateColumn.CellTemplate><DataTemplate><StackPanel Orientation='Horizontal'>";
string templateColumnEnd = #"</StackPanel></DataTemplate></DataGridTemplateColumn.CellTemplate></local:MyDataGridTemplateColumn>";
string radioButtonString = #"<RadioButton Name='rb1' Content='1' IsChecked='false'/>";
string fullXamlString = templateColumnStart + radioButtonString + templateColumnEnd;
using (MemoryStream stream = new MemoryStream(ASCIIEncoding.UTF8.GetBytes(fullXamlString)))
{
MyDataGridTemplateColumn templateColumn = (MyDataGridTemplateColumn)XamlReader.Load(stream);
templateColumn.ElementGenerated += (ss, ee) =>
{
ContentPresenter cc = ee.Content as ContentPresenter;
if(cc != null)
{
StackPanel sp = VisualTreeHelper.GetChild(cc, 0) as StackPanel;
if(sp != null)
{
RadioButton rb = sp.FindName("rb1") as RadioButton;
if (rb != null)
rb.Checked += rb_Checked;
}
}
};
myDataGrid.Columns.Add(templateColumn);
}
Note that the you will have to change "assembly=WpfApplication2" to the name of the assembly where you define your MyDataGridTemplateColumn class. The same applies to the namespace.
You can add EventSetter to your window like this:
<Window.Resources>
<Style TargetType="{x:Type RadioButton}">
<EventSetter Event="Checked" Handler="RadioButton_Checked"/>
</Style>
</Window.Resources>
Event handler method:
void RadioButton_Checked(object sender, RoutedEventArgs e)
{
// Handle the event...
}
This will call the handler for every RadioButton in your DataGrid.
If I remember correctly, you can add Checked="CheckHandlerFunction" in the XAML itself. That may work.
This may not apply in your situation, but have you looked into templates and binding controls to DataContext? You can automate a lot of control creation in WPF using them.

Silverlight dependancy property is not notifying in custom control

Scenario
I have a custom combo box where i have a label in the Combobox selection box. I need to change the label as I noted in the second image. But I want to do it only when I select items by selecting the check box. I can select multiple items, so the label should be updated as a comma separated value of selected items. If there is not enough space to display the full text of the label there should be "..." symbol to indicate that there are more items selected in the combo box.
I created a custom Label by inheriting the text Box control where I do all the changes in the callback event of a Dependency property. (Check custom Text Box code)
Now the problem is that the callback event in the custom Text box control is not firing when I change the bounded property in the View model (I am doing this by adding values to the observable collection in the code behind in check box on Check event. Please Check check box event code).
I can see that first time when I load default data in the view model the line is hit by the break point in the "Getter" part of "SelectedFilterResources" . But I never get a hit in the Setter part of the property.
Custom Text Box
The custom text box has the "CaptionCollectionChanged" callback event. It is the place where I have all logic to achieve my scenario. "Resources item" here is a type of Model.
public class ResourceSelectionBoxLable : TextBox
{
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
IsReadOnly = true;
}
public static List<ResourceItem> LocalFilterdResources = new List<ResourceItem>();
#region Dependancy Properties
public static readonly DependencyProperty FilterdResourcesProperty =
DependencyProperty.Register("SelectedFilterdResources",
typeof (ObservableCollection<ResourceItem>),
typeof (ResourceSelectionBoxLable),
new PropertyMetadata(new ObservableCollection<ResourceItem>(),
CaptionCollectionChanged));
public ObservableCollection<ResourceItem> SelectedFilterdResources
{
get
{
return
(ObservableCollection<ResourceItem>) GetValue(FilterdResourcesProperty);
}
set
{
SetValue(FilterdResourcesProperty, value);
LocalFilterdResources = new List<ResourceItem>(SelectedFilterdResources);
}
}
#endregion
private static void CaptionCollectionChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var resourceSelectionBoxLable = d as ResourceSelectionBoxLable;
if (resourceSelectionBoxLable != null)
{
if (LocalFilterdResources.Count <= 0)
{
resourceSelectionBoxLable.Text = "Resources"
}
else
{
var actualwidthOflable = resourceSelectionBoxLable.ActualWidth;
var newValue = e.NewValue as string;
//Get the Wdith of the Text in Lable
TextBlock txtMeasure = new TextBlock();
txtMeasure.FontSize = resourceSelectionBoxLable.FontSize;
txtMeasure.Text = newValue;
double textwidth = txtMeasure.ActualWidth;
//True if Text reach the Limit
if (textwidth > actualwidthOflable)
{
var appendedString = string.Join(", ",
LocalFilterdResources.Select(item => item.ResourceCaption)
.ToArray());
resourceSelectionBoxLable.Text = appendedString;
}
else
{
if (LocalFilterdResources != null)
{
var morestring = string.Join(", ",
(LocalFilterdResources as IEnumerable<ResourceItem>).Select(item => item.ResourceCaption)
.ToArray());
var subsring = morestring.Substring(0, Convert.ToInt32(actualwidthOflable) - 4);
resourceSelectionBoxLable.Text = subsring + "...";
}
}
}
}
}
}
Custom Combo Box.
This is the control where I use the above custom label. This is also a custom control so most of the properties and styles in this controls are custom made. "DPItemSlectionBoxTemplate" is a dependency property where I enable the Selection Box of the combo box by adding an attached property to the control template. This control works fine, because I use this control in other places in my system for different purposes.
<styles:CommonMultiComboBox
x:Name="Resourcescmb" IsEnabled="{Binding IsResourceComboEnable,Mode=TwoWay}"
IsTabStop="False"
>
<styles:CommonMultiComboBox.ItemDataTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelect, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Click="CheckBox_Click"
Content="{Binding ResourceCaption}"
Style="{StaticResource CommonCheckBoxStyle}"
Tag ="{Binding}"
Checked="Resource_ToggleButton_OnChecked" />
</DataTemplate>
</styles:CommonMultiComboBox.ItemDataTemplate>
<styles:CommonMultiComboBox.DPItemSlectionBoxTemplate>
<DataTemplate>
<filtersTemplate:ResourceSelectionBoxLable
Padding="0"
Height="15"
FontSize="10"
SelectedFilterdResources="{Binding DataContext.FilterdResources,ElementName=root ,Mode=TwoWay}" />
</DataTemplate>
</styles:CommonMultiComboBox.DPItemSlectionBoxTemplate>
</styles:CommonMultiComboBox>
ViewModel
private ObservableCollection<ResourceItem> _resourceItems;
public ObservableCollection<ResourceItem> FilterdResources
{
get { return _resourceItems; }
set
{
SetOnChanged(value, ref _resourceItems, "FilterdResources");
}
}
Constructor of View Model
FilterdResources=new ObservableCollection<ResourceItem>();
"SetOnChanged" is a method in the View Model base class where we have the INotifyPropertichanged implementation.
Check Box Event
private void Resource_ToggleButton_OnChecked(object sender, RoutedEventArgs e)
{
var senderControl = sender as CheckBox;
if(senderControl==null)
return;
var selectedContent=senderControl.Tag as ResourceItem;
if (selectedContent != null)
{
ViewModel.FilterdResources.Add(selectedContent);
}
}
I can access the View Model from the Code behind through the View Model Property.
Why is the call back event not notified when I change bounded values? Am i missing something here? Dependency properties are supposed to work for two way bindings aren't they? Could any one please help me on this?
Thanks in advance.
Looks like your issue is that you're expecting the CaptionCollectionChanged event to fire when the bound collection is changed (i.e. items added or removed). When in fact this event will fire only when you're changing an instance of the bound object.
What you need to do here is to subscribe to ObservableCollection's CollectionChanged event in the setter or change callback (which you already have - CaptionCollectionChanged) of your dependency property.
public static readonly DependencyProperty FilterdResourcesProperty =
DependencyProperty.Register("SelectedFilterdResources",
typeof (ObservableCollection<ResourceItem>),
typeof (ResourceSelectionBoxLable),
new PropertyMetadata(new ObservableCollection<ResourceItem>(),
CaptionCollectionChanged));
private static void CaptionCollectionChanged(DependencyObject d,
DependencyPropertyChangedEventArgs args)
{
var collection = args.NewValue as INotifyCollectionChanged;
if (collection != null)
{
var sender = d as ResourceSelectionBoxLable;
if (sender != null)
{
collection.CollectionChanged += sender.BoundItems_CollectionChanged;
}
}
}
private void BoundItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// Do your control logic here.
}
Don't forget to add cleanup logic - unsubscribe from collection change when collection instance is changed and so on.

Selecting A Series Of Text Boxes

I have a series(10) of selectable TextBoxes. I need to be able to click on one of them and select all the text boxes on which the mouse is moved until the click is released.
I used the following code but I am unable to hit the MouseMove on the other TextBoxes. It always hits the TextBox on which the Click was made.
class SelectableTextBox: TextBox
{
public Boolean IsSelected { get; protected set; }
public void select(Boolean value)
{
this.IsSelected = value;
if (value)
{
this.Background = System.Windows.Media.Brushes.Aqua;
}
else
{
this.Background = System.Windows.Media.Brushes.White;
}
}
}
private void onPreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
SelectableTextBox textBox = (SelectableTextBox)sender;
this.SelectionStartedRight = !textBox.IsSelected;
textBox.select(!textBox.IsSelected);
}
private void onPreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
SelectableTextBox textBox = (SelectableTextBox)sender;
if (this.SelectionStartedRight)
{
textBox.select(true);
}
}
private void onPreviewMouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
SelectableTextBox textBox = (SelectableTextBox)sender;
this.SelectionStartedRight = false;
}
Try using MouseEnter event instead of MouseMove. Attach MouseEnter to your selectable textboxes. That would ensure that only they trigger the desired event handler and its code.
If you decide to stay with the global handler, be careful when you convert sender to a specific control type. You need to account for those times when it's not the "expected" control:
SelectableTextBox textBox = sender as SelectableTextBox;
if (textBox != null)
{
// The rest of the code here...
}
select is a Linq keyword, so you might want to rename that method to avoid any conflicts down the road. While I don't think it'd be causing any issues, I would change it.
You don't have to follow this convention, but in C# its customary to use an uppercase for the first letter of a method. Also note that the default access modifier for a class is internal... just want to make sure you're aware of that, as you continue your development.
Your last method, onPreviewMouseLeftButtonUp(...) has the following code in it:
SelectableTextBox textBox = (SelectableTextBox)sender;
Not only is it unsafe, as I've described above, but it also does absolutely nothing.
Lastly... and this is just me, I would probably move the code that handles changing the state of a selectable textbox (selected or not selected) into its class, since it belongs there. Why should anything else be in charge of how it handles its state. Keep things where they belong and you'll have a much easier time testing, debugging and maintaining your code. Don't fall into the "I'll refactor it later" trap... it'll rarely happen.
Here's my crude example. I let the class handle its MouseEnter event and simply check if the Mouse.LeftButton is down at that time. You would have to expand on it, but it should be a solid start:
Made some edits per OP's requests in the comments.
Preview:
XAML:
<Window x:Class="SelectableTextBoxes.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SelectableTextBoxes"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
</StackPanel>
</Window>
C# (Pardon me for putting the SelectableTextBox into the same file...):
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace SelectableTextBoxes
{
// Move this class into its own file (it's here for prototyping).
public class SelectableTextBox : TextBox
{
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_isSelected = value;
// If the value changes, sets new color.
SetSelectionColor();
}
}
}
public SelectableTextBox()
{
// For handling an initial click if it happens in the textbox.
PreviewMouseDown += SelectableTextBox_PreviewMouseDown;
// For handling selection when mouse enters the textbox
// and left mouse button is down.
MouseEnter += SelectableTextBox_MouseEnter;
// To handle mouse capture (release it).
GotMouseCapture += SelectableTextBox_GotMouseCapture;
}
// Handles the mouse down event within the textbox.
void SelectableTextBox_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (!IsSelected)
{
IsSelected = true;
}
// If one of the Shift keys is down, return, since
// we don't want to deselect others.
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
{
return;
}
// This part makes a poor assumption that the parent is
// always going to be a Panel... expand on this code to
// cover other types that may contain more than one element.
var parent = VisualTreeHelper.GetParent(this) as Panel;
if (parent != null)
{
foreach (var child in parent.Children)
{
// If a child is not of a correct type, it'll be null.
var tbx = child as SelectableTextBox;
// This is where we check to see if it's null or this instance.
if (tbx != null && tbx != this)
{
tbx.IsSelected = false;
}
}
}
}
// When textbox receives focus, this event fires... we need to release
// the mouse to continue selection.
void SelectableTextBox_GotMouseCapture(object sender, MouseEventArgs e)
{
ReleaseMouseCapture();
}
// Sets selection state to true if the left mouse button is
// down while entering.
void SelectableTextBox_MouseEnter(object sender, MouseEventArgs e)
{
if (Mouse.LeftButton == MouseButtonState.Pressed)
{
IsSelected = true;
}
}
// Sets the background color based on selection state.
private void SetSelectionColor()
{
if (IsSelected)
{
Background = Brushes.LightCyan;
}
else
{
Background = Brushes.White;
}
}
}
// Window code... should be on its own, but I placed the two
// classes together while prototyping.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}

Categories