I am creating a simple AutoComplete TextBox and have a list of values which If the user starts to enter any characters in them string, the appropriate string will appear.
Now I have created my Textbox with a Binding property to my ViewModel:
<TextBox Text="{Binding ServerURL, UpdateSourceTrigger=PropertyChanged}" />
So when the user enters a new character It will trigger my property to get fired and therefore fire a method which will retrieve the values it relates to.
private string _serverURL;
public string ServerURL {
get { return _serverURL; }
set
{
_serverURL = value;
ServerURL_TextChanged();
OnPropertyChanged("ServerURL");
}
}
The method will then just populate a ListBox with the results that string refers to.
When I select a value from the ListBox i want to set the Full string value to the TextBox text property, but when i do this it triggers the method ServerURL_TextChanged().
Is there a way I can set the ServerURL property, but not to fire the method inside it?
For a solution there is a need to separate the ways with which you can set ServerURL property.
public string ServerURL {
get { return _serverURL; }
set
{
setServerURL(value, isSetByUser = true);
}
}
private function void setServerURL(string value, bool isSetByUser){
_serverURL = value;
ServerURL_TextChanged(isSetByUser);
OnPropertyChanged("ServerURL");
}
When the List is changed you can call from the code setServerURL(someValue, isSetByUser = false);
And then in ServerURL_TextChanged implementation decide what to do with it.
The easiest way to implement this functionality is to handle TextChanged events from the code-behind, where you will have full control of the UI for this kind of decision making. It does not violate MVVM principles to manage UI operations from the code-behind.
Here is an example of such a code-behind implementation. You may find it useful.
public partial class AutoCompleteComboBox : UserControl
{
private Window w;
public AutoCompleteComboBox()
{
InitializeComponent();
}
~AutoCompleteComboBox()
{
if(w == null)
{
return;
}
else
{
w.MouseLeftButtonDown -= Window_MouseLeftDown;
}
}
#region Behaviours
public void FocusTextBox()
{
txt.Focus();
txt.CaretIndex = txt.Text.Length;
}
#endregion
#region DependencyProperties
public static readonly DependencyProperty InputPaddingProperty =
DependencyProperty.Register(
"InputPadding",
typeof(Thickness),
typeof(AutoCompleteComboBox)
);
public Thickness InputPadding
{
get
{
return (Thickness)GetValue(InputPaddingProperty);
}
set
{
SetValue(InputPaddingProperty, value);
}
}
public static readonly DependencyProperty TextBoxHeightProperty =
DependencyProperty.Register(
"TextBoxHeight",
typeof(double),
typeof(AutoCompleteComboBox)
);
public double TextBoxHeight
{
get
{
return (double)GetValue(TextBoxHeightProperty);
}
set
{
SetValue(TextBoxHeightProperty, value);
}
}
public static readonly DependencyProperty ItemPanelMaxHeightProperty =
DependencyProperty.Register(
"ItemPanelMaxHeight",
typeof(double),
typeof(AutoCompleteComboBox)
);
public double ItemPanelMaxHeight
{
get
{
return (double)GetValue(ItemPanelMaxHeightProperty);
}
set
{
SetValue(ItemPanelMaxHeightProperty, value);
}
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register(
"ItemsSource",
typeof(IEnumerable),
typeof(AutoCompleteComboBox)
);
public IEnumerable ItemsSource
{
get
{
return (IEnumerable)ItemsSource;
}
set
{
SetValue(ItemsSourceProperty, value);
}
}
public static readonly DependencyProperty DisplayMemberPathProperty =
DependencyProperty.Register(
"DisplayMemberPath",
typeof(string),
typeof(AutoCompleteComboBox)
);
public string DisplayMemberPath
{
get
{
return GetValue(DisplayMemberPathProperty).ToString();
}
set
{
SetValue(DisplayMemberPathProperty, value);
}
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(
"Text",
typeof(string),
typeof(AutoCompleteComboBox),
new FrameworkPropertyMetadata(
"",
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault
)
);
public string Text
{
get
{
return GetValue(TextProperty).ToString();
}
set
{
SetValue(TextProperty, value);
}
}
public string TargetValue { get; set; } = "";
public static readonly DependencyProperty IsDropDownOpenProperty =
DependencyProperty.Register(
"IsDropDownOpen",
typeof(bool),
typeof(AutoCompleteComboBox),
new FrameworkPropertyMetadata(
false,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault
)
);
public bool IsDropDownOpen
{
get
{
return (bool)GetValue(IsDropDownOpenProperty);
}
set
{
SetValue(IsDropDownOpenProperty, value);
}
}
#endregion
#region Events
private void me_Loaded(object sender, RoutedEventArgs e)
{
w = VisualTreeHelpers.FindAncestor<Window>(this);
w.MouseLeftButtonDown += Window_MouseLeftDown;
FocusTextBox();
}
private void Window_MouseLeftDown(object sender, MouseButtonEventArgs e)
{
IsDropDownOpen = false;
}
private void lst_KeyDown(object sender, KeyEventArgs e)
{
}
private void lst_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (TargetValue != null && TargetValue.Trim().Length > 0)
{
txt.Text = TargetValue;
IsDropDownOpen = false;
}
FocusTextBox();
}
private void lst_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
}
private void lst_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (lst.SelectedItem != null)
{
TargetValue = lst.SelectedItem.ToString();
}
}
private void txt_LostFocus(object sender, RoutedEventArgs e)
{
if (lst.IsFocused == false)
{
IsDropDownOpen = false;
FocusTextBox();
}
}
private void lst_LostFocus(object sender, RoutedEventArgs e)
{
MessageBox.Show("text changed");
if (txt.IsFocused == false)
{
IsDropDownOpen = false;
}
}
private void txt_TextChanged(object sender, TextChangedEventArgs e)
{
IsDropDownOpen = true;
}
private void txt_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (IsDropDownOpen && lst.Items.Count > 0)
{
if (lst.SelectedIndex < 0)
{
lst.SelectedIndex = 0;
}
if (e.Key == Key.Up && lst.SelectedIndex > 0)
{
lst.SelectedIndex--;
}
else if (e.Key == Key.Down && lst.SelectedIndex < lst.Items.Count - 1)
{
lst.SelectedIndex++;
}
else if(e.Key == Key.Enter || e.Key == Key.Tab)
{
if(lst.SelectedIndex > -1)
{
txt.Text = TargetValue;
IsDropDownOpen = false;
FocusTextBox();
}
}
}
}
#endregion
}
And here's the XAML
<UserControl x:Class="SHARED_COMPONENTS.AutoCompleteComboBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
x:Name="me"
Loaded="me_Loaded"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<TextBox
x:Name="txt"
Background="CornflowerBlue"
Foreground="White"
Grid.Row="0"
Text="{Binding ElementName=me, Path=Text,UpdateSourceTrigger=PropertyChanged}"
TextChanged="txt_TextChanged" PreviewKeyDown="txt_PreviewKeyDown"
Height="{Binding ElementName=me, Path=ActualHeight}"
Padding="{Binding ElementName=me,Path=InputPadding}"
/>
<Popup IsOpen="{Binding ElementName=me, Path=IsDropDownOpen}" ClipToBounds="False">
<Border Grid.Row="1">
<Border.Effect>
<DropShadowEffect Color="Black" />
</Border.Effect>
<ListBox
x:Name="lst"
Grid.Row="1"
ItemsSource="{Binding ElementName=me, Path=ItemsSource}"
PreviewKeyDown="lst_KeyDown"
SelectionChanged="lst_SelectionChanged"
PreviewMouseLeftButtonDown="lst_MouseLeftButtonDown"
PreviewMouseLeftButtonUp="lst_PreviewMouseLeftButtonUp"
DisplayMemberPath="{Binding ElementName=me, Path=DisplayMemberPath }"
ClipToBounds="False"
>
<ListBox.Style>
<Style TargetType="ListBox">
<Setter Property="Background" Value="#f0f0f0" />
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ElementName=lst, Path=HasItems}" Value="True" />
<Condition Binding="{Binding ElementName=me, Path=IsDropDownOpen}" Value="True" />
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" Value="Visible" />
</MultiDataTrigger>
<DataTrigger Binding="{Binding ElementName=me, Path=IsDropDownOpen}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.Style>
<ListBox.Resources>
<Style TargetType="ListBoxItem">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsMouseOver}" Value="True">
<Setter Property="IsSelected" Value="True" />
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsSelected}" Value="True">
<Setter Property="Foreground" Value="CornflowerBlue" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.Resources>
</ListBox>
</Border>
</Popup>
</Grid>
</Grid>
</UserControl>
Related
Here is my searchtextbox class, the actual textbox inside the XAML design is PART_TextBox, I have tried using PART_TextBox_TextChanged, TextBox_TextChanged, and OnTextBox_TextChanged, none work, and .text is empty always.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SearchTextBox
{
[TemplatePart(Name = PartRootPanelName, Type = typeof(Panel))]
[TemplatePart(Name = PartTextBoxName, Type = typeof(TextBox))]
[TemplatePart(Name = PartSearchIconName, Type = typeof(Button))]
[TemplatePart(Name = PartCloseButtonName, Type = typeof(Button))]
public class SearchTextBox : Control
{
private const string PartRootPanelName = "PART_RootPanel";
private const string PartTextBoxName = "PART_TextBox";
private const string PartSearchIconName = "PART_SearchIcon";
private const string PartCloseButtonName = "PART_CloseButton";
// Commands.
public static readonly RoutedCommand ActivateSearchCommand;
public static readonly RoutedCommand CancelSearchCommand;
// Properties.
public static readonly DependencyProperty HandleClickOutsidesProperty;
public static readonly DependencyProperty UpdateDelayMillisProperty;
public static readonly DependencyProperty HintTextProperty;
public static readonly DependencyProperty DefaultFocusedElementProperty;
public static readonly DependencyProperty TextBoxTextProperty;
public static readonly DependencyProperty TextBoxTextChangedProperty;
static SearchTextBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(SearchTextBox), new FrameworkPropertyMetadata(typeof(SearchTextBox)));
ActivateSearchCommand = new RoutedCommand();
CancelSearchCommand = new RoutedCommand();
// // Using of CommandManager.
// // https://www.codeproject.com/Articles/43295/ZoomBoxPanel-add-custom-commands-to-a-WPF-control
CommandManager.RegisterClassCommandBinding(typeof(SearchTextBox),
new CommandBinding(ActivateSearchCommand, ActivateSearchCommand_Invoke));
CommandManager.RegisterClassCommandBinding(typeof(SearchTextBox),
new CommandBinding(CancelSearchCommand, CancelSearchCommand_Invoke));
// Register properties.
HandleClickOutsidesProperty = DependencyProperty.Register(
nameof(HandleClickOutsides), typeof(bool), typeof(SearchTextBox),
new UIPropertyMetadata(true));
// // Set default value.
// // https://stackoverflow.com/questions/6729568/how-can-i-set-a-default-value-for-a-dependency-property-of-type-derived-from-dep
UpdateDelayMillisProperty = DependencyProperty.Register(
nameof(UpdateDelayMillis), typeof(int), typeof(SearchTextBox),
new UIPropertyMetadata(1000));
HintTextProperty = DependencyProperty.Register(
nameof(HintText), typeof(string), typeof(SearchTextBox),
new UIPropertyMetadata("Set HintText property"));
DefaultFocusedElementProperty = DependencyProperty.Register(
nameof(DefaultFocusedElement), typeof(UIElement), typeof(SearchTextBox));
TextBoxTextProperty =
DependencyProperty.Register(
"Text",
typeof(string),
typeof(SearchTextBox));
}
public string Text
{
get { return (string)GetValue(TextBoxTextProperty); }
set { SetValue(TextBoxTextProperty, value); }
}
public static void ActivateSearchCommand_Invoke(object sender, ExecutedRoutedEventArgs e)
{
if (sender is SearchTextBox self)
self.ActivateSearch();
}
public static void CancelSearchCommand_Invoke(object sender, ExecutedRoutedEventArgs e)
{
if (sender is SearchTextBox self)
{
self.textBox.Text = "";
self.CancelPreviousSearchFilterUpdateTask();
self.UpdateFilterText();
self.DeactivateSearch();
}
}
private static UIElement GetFirstSelectedControl(Selector list)
=> list.SelectedItem == null ? null :
list.ItemContainerGenerator.ContainerFromItem(list.SelectedItem) as UIElement;
private static UIElement GetDefaultSelectedControl(Selector list)
=> list.ItemContainerGenerator.ContainerFromIndex(0) as UIElement;
// Events.
// // Using of events.
// // https://stackoverflow.com/questions/13447940/how-to-create-user-define-new-event-for-user-control-in-wpf-one-small-example
public event EventHandler SearchTextFocused;
public event EventHandler SearchTextUnfocused;
// // Parameter passing:
// // https://stackoverflow.com/questions/4254636/how-to-create-a-custom-event-handling-class-like-eventargs
public event EventHandler<string> SearchRequested;
public event TextChangedEventHandler TextChanged;
// Parts.
private Panel rootPanel;
private TextBox textBox;
private Button searchIcon;
private Label closeButton;
// Handlers.
// Field for click-outsides handling.
private readonly MouseButtonEventHandler windowWideMouseButtonEventHandler;
// Other fields.
private CancellationTokenSource waitingSearchUpdateTaskCancellationTokenSource;
// <init>
public SearchTextBox()
{
// Click events in the window will be previewed by
// function OnWindowWideMouseEvent (defined later)
// when the handler is on. Now it's off.
windowWideMouseButtonEventHandler =
new MouseButtonEventHandler(OnWindowWideMouseEvent);
}
// Properties.
public bool HandleClickOutsides
{
get => (bool)GetValue(HandleClickOutsidesProperty);
set => SetValue(HandleClickOutsidesProperty, value);
}
public int UpdateDelayMillis
{
get => (int)GetValue(UpdateDelayMillisProperty);
set => SetValue(UpdateDelayMillisProperty, value);
}
public string HintText
{
get => (string)GetValue(HintTextProperty);
set => SetValue(HintTextProperty, value);
}
public UIElement DefaultFocusedElement
{
get => (UIElement)GetValue(DefaultFocusedElementProperty);
set => SetValue(DefaultFocusedElementProperty, value);
}
//Event handler functions.
// This would only be on whenever search box is focused.
private void OnWindowWideMouseEvent(object sender, MouseButtonEventArgs e)
{
// By clicking outsides the search box deactivate the search box.
if (!IsMouseOver) DeactivateSearch();
}
private void PART_TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextChanged?.Invoke(this, e);
}
public void OnTextBox_GotFocus(object sender, RoutedEventArgs e)
{
SearchTextFocused?.Invoke(this, e);
if (HandleClickOutsides)
// Get window.
// https://stackoverflow.com/questions/302839/wpf-user-control-parent
Window.GetWindow(this).AddHandler(
Window.PreviewMouseDownEvent, windowWideMouseButtonEventHandler);
}
public void OnTextBox_LostFocus(object sender, RoutedEventArgs e)
{
SearchTextUnfocused?.Invoke(this, e);
if (HandleClickOutsides)
Window.GetWindow(this).RemoveHandler(
Window.PreviewMouseDownEvent, windowWideMouseButtonEventHandler);
}
private void OnTextBox_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
CancelSearchCommand.Execute(null, this);
else if (e.Key == Key.Enter)
{
CancelPreviousSearchFilterUpdateTask();
UpdateFilterText();
}
else
{
CancelPreviousSearchFilterUpdateTask();
// delay == 0: Update now;
// delay < 0: Don't update except Enter or Esc;
// delay > 0: Delay and update.
var delay = UpdateDelayMillis;
if (delay == 0) UpdateFilterText();
else if (delay > 0)
{
// // Delayed task.
// // https://stackoverflow.com/questions/15599884/how-to-put-delay-before-doing-an-operation-in-wpf
waitingSearchUpdateTaskCancellationTokenSource = new CancellationTokenSource();
Task.Delay(delay, waitingSearchUpdateTaskCancellationTokenSource.Token)
.ContinueWith(self => {
if (!self.IsCanceled) Dispatcher.Invoke(() => UpdateFilterText());
});
}
}
}
// Public interface.
public void ActivateSearch()
{
textBox?.Focus();
}
public void DeactivateSearch()
{
// // Use keyboard focus instead.
// // https://stackoverflow.com/questions/2914495/wpf-how-to-programmatically-remove-focus-from-a-textbox
//Keyboard.ClearFocus();
if (DefaultFocusedElement != null)
{
UIElement focusee = null;
if (DefaultFocusedElement is Selector list)
{
focusee = GetFirstSelectedControl(list);
if (focusee == null)
focusee = GetDefaultSelectedControl(list);
}
if (focusee == null) focusee = DefaultFocusedElement;
Keyboard.Focus(focusee);
}
else
{
rootPanel.Focusable = true;
Keyboard.Focus(rootPanel);
rootPanel.Focusable = false;
}
}
// Helper functions.
private void CancelPreviousSearchFilterUpdateTask()
{
if (waitingSearchUpdateTaskCancellationTokenSource != null)
{
waitingSearchUpdateTaskCancellationTokenSource.Cancel();
waitingSearchUpdateTaskCancellationTokenSource.Dispose();
waitingSearchUpdateTaskCancellationTokenSource = null;
}
}
private void UpdateFilterText() => SearchRequested?.Invoke(this, textBox.Text);
// .
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
// // Idea of detaching.
// // https://www.jeff.wilcox.name/2010/04/template-part-tips/
if (textBox != null)
{
textBox.GotKeyboardFocus -= OnTextBox_GotFocus;
textBox.LostKeyboardFocus -= OnTextBox_LostFocus;
textBox.KeyUp -= OnTextBox_KeyUp;
}
rootPanel = GetTemplateChild(PartRootPanelName) as Panel;
textBox = GetTemplateChild(PartTextBoxName) as TextBox;
searchIcon = GetTemplateChild(PartSearchIconName) as Button;
closeButton = GetTemplateChild(PartCloseButtonName) as Label;
if (textBox != null)
{
textBox.GotKeyboardFocus += OnTextBox_GotFocus;
textBox.LostKeyboardFocus += OnTextBox_LostFocus;
textBox.KeyUp += OnTextBox_KeyUp;
}
}
}
}
My XAML:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SearchTextBox" xmlns:componentModel="clr-namespace:System.ComponentModel;assembly=WindowsBase">
<Style TargetType="{x:Type local:SearchTextBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:SearchTextBox}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid Name="PART_RootPanel">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="28"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<TextBox Name="PART_TextBox" Background="#FF333337" BorderThickness="0" VerticalContentAlignment="Center" Foreground="White" FontSize="14px" Text=""/>
<TextBlock IsHitTestVisible="False" Text="{TemplateBinding HintText}" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="4,0,0,0" FontSize="14px" Foreground="#FF7C7777">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=PART_TextBox}" Value="">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
<Button Width="28" Grid.Column="1" Name="PART_SearchIcon" Content="🔍" Background="#FF252526"
Focusable="False" Command="{x:Static local:SearchTextBox.ActivateSearchCommand}">
<Button.Template>
<ControlTemplate TargetType="Button">
<Grid Background="#FF333337">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>
</Button.Template>
<Button.Style>
<Style TargetType="Button">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=PART_TextBox}" Value="">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Label Grid.Column="1" Width="28" HorizontalAlignment="Center" HorizontalContentAlignment="Center" Cursor="Hand" VerticalContentAlignment="Center" VerticalAlignment="Stretch" Margin="0" Padding="4" Foreground="White" FontWeight="Bold" Content="x" Name="PART_CloseButton" Focusable="False"
Background="#FF333337">
<Label.InputBindings>
<MouseBinding Command="{x:Static local:SearchTextBox.CancelSearchCommand}" MouseAction="LeftClick" />
</Label.InputBindings>
<Label.Style>
<Style TargetType="Label">
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=PART_TextBox}" Value="">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#000"/>
</Trigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The control builds fine, but I cannot get searchTextBox.Text, it returns null, and on TextChanged does not fire. Any ideas?
I would suggest that you do a bit more research and learning about the code you're using before you copy and paste it. Try to understand what it's doing and why before use it.
You don't reference your method PART_TextBox_TextChanged anywhere, it's not used in OnApplyTemplate where I would expect it to be assigned as a handler. Without that, it will never be called. I would expect a textBox.TextChanged -= PART_TextBox_TextBox_TextChanged in the first if statement, and a textBox.TextChanged += PART_TextBox_TextBox_TextChanged in the second.
You never set the value of your TextBoxText dependency property anywhere; neither in code nor with a Binding or TemplateBinding. Your Text property is referencing TextBoxText for its value, which would always be null if it's never set. public static readonly DependencyProperty TextBoxTextProperty; (along with the code in static SearchTextBox()) declares that a dependency property exists, but the value of it is never set.
In a custom control like this, you can bind the Text property of PART_TextBox to your TextBoxText property using a TemplateBinding like so:
<TextBox Name="PART_TextBox" ... Text="{TemplateBinding TextBoxText}"/>
I created a WPF application where I create a list of items to be executed, as a Treeview. On a click event, I parse the ObservableCollection items one by one. This observableCollection is set as the DataContext for the treeview. When running the tests, I want to highlight the current running item in the Treeview.
I have the implemented following code, but the highlighting on the Treeview (visuallY) does not seem to happen. I checked that the "IsSelected" property does get set/unset as programmed.
I am not sure were I went wrong. Could you point out where the mistake is.
I have this class used as a DataContext to the TreeView (named mainTree).
class mytreefile : INotifyPropertyChanged
{
private string _name { get; set; }
public ObservableCollection <mytreefile> children { get; set; }
bool? _isSelected = false;
public bool? IsSelected
{
get { return _isSelected; }
set { SetIsSelected(value); }
}
void SetIsSelected(bool? val)
{
_isSelected = val;
}
public mytreefile(string value)
{
_name = value;
children = new ObservableCollection<mytreefile>();
}
void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
The XAML file is
<Grid.Resources>
<ResourceDictionary>
<HierarchicalDataTemplate x:Key="tvTemplate" ItemsSource="{Binding children, Mode=TwoWay}">
<StackPanel Orientation="Horizontal">
<ContentPresenter Content="{Binding _name, Mode=TwoWay}" Margin="2,0" />
</StackPanel>
</HierarchicalDataTemplate>
</ResourceDictionary>
</Grid.Resources>
<TreeView x:Name="mainTree" Grid.Row="0" Grid.Column="0" Grid.RowSpan="4" Background="WhiteSmoke"
Height="Auto" Width="Auto" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Margin="1,0,2,0" SelectedItemChanged="mainTree_SelectedItemChanged"
ItemTemplate="{StaticResource tvTemplate}"
ItemsSource="{Binding}" DataContext="{Binding nodes}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
<Trigger Property="IsSelected" Value="False">
<Setter Property="FontWeight" Value="Normal" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
And my MainWindow code is:
public partial class MainWindow : Window
{
ObservableCollection<mytreefile> nodes = new ObservableCollection<mytreefile>();
mytreefile mtf = null;
Thread thThread = null;
int gnCount = 0;
private void LoadTree ()
{
mytreefile tf1 = new mytreefile("Group1");
nodes.Add(tf1);
mytreefile subtf1 = new mytreefile("Sub Item 1");
mytreefile subtf2 = new mytreefile("Sub Item 2");
mytreefile subtf3 = new mytreefile("Sub Item 3");
mytreefile subtf4 = new mytreefile("Sub Item 4");
tf1.children.Add(subtf1); tf1.children.Add(subtf2); tf1.children.Add(subtf3); tf1.children.Add(subtf4);
maintree.DataContext = nodes;
}
private void OnButton1_click()
{
mtf = nodes.ElementAt(0);
gnCount = 0;
thThread = new Thread(new ThreadStart(this.myThread));
thThread.Start();
}
public void myThread ()
{
for (int i = 0; i < 3; i++)
{
Thread.Sleep(1000);
this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Send,
new Action(() => SetTreeItem(i)));
}
}
public void SetTreeItem(int i)
{
if (gnCount > 0) {
mytreefile mtreeitem = mtf.children.ElementAt(gnCount-1);
mtreeitem.IsSelected = false;
}
mytreefile mtreeitem = mtf.children.ElementAt(gnCount++);
mtreeitem.IsSelected = true;
}
}
The problem was with the "mytreefile" class.
The below class works fine. The way the "IsSelected" implementation was done made the difference. Posting the code for reference.
class mytreefile : INotifyPropertyChanged
{
private string _name { get; set; }
public ObservableCollection <mytreefile> children { get; set; }
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (value != this._isSelected)
{
this._isSelected = value;
NotifyPropertyChanged("IsSelected");
}
}
}
public mytreefile(string value)
{
_name = value;
children = new ObservableCollection<mytreefile>();
}
void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
I have a view model that presents items in ListBox. There are two collection: Source contains all element and Checked contains only checked elements. There are two buttons SelectAll and ClearAll. When I push on one of this button View Model works well and both Source and Checked collections updating, but no changes in Listbox happens.
CheckItemPresenterVM<T> - an element that save the state of one button and implements INotifyPropertyChange, but when property IsChecked changes CollectionChangedevent doesn't reise.
The question is how to make it work?
<UserControl.Resources>
<ItemsPanelTemplate x:Key="listPanelTemplate">
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
<Style TargetType="ListBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ListBoxItemIsCheckedBinding"
TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected"
Value="{Binding IsChecked, Mode=TwoWay}" />
<Setter Property="Padding"
Value="5,3" />
<Setter Property="Background"
Value="LightCyan"></Setter>
</Style>
<Style TargetType="Button">
<Setter Property="Padding"
Value="7, 3" />
<Setter Property="Margin"
Value="1" />
</Style>
<Style TargetType="ToggleButton">
<Setter Property="Padding"
Value="7, 3" />
<Setter Property="Margin"
Value="1" />
</Style>
</UserControl.Resources>
<Grid>
<Expander Header="Categories"
IsExpanded="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<WrapPanel Grid.Row="0"
Margin="0, 5">
<Button Command="{Binding SelectAll}">Select All</Button>
<Button Command="{Binding ClearAll}">Clear All</Button>
<Button Command="{Binding InvertSelection}">Invert Selection</Button>
</WrapPanel>
<ListBox Grid.Row="1" />
<ListBox Name="CategoriesListBox"
Grid.Row="2"
SelectionMode="Multiple"
ItemsSource="{Binding CategoryVM.Source, Mode=TwoWay}"
ItemContainerStyle="{DynamicResource ListBoxItemIsCheckedBinding}"
ItemsPanel="{StaticResource listPanelTemplate}" />
<ListBox Name="CheckedListBox"
Grid.Row="3"
SelectionMode="Multiple"
ItemsSource="{Binding CategoryVM.Checked}"/>
</Grid>
</Expander>
</Grid>
class TestViewModel
{
public CheckItemVM<string> CategoryVM { get; set; }
public TestViewModel()
{
logger.FileName = "TestSelectionFilter.txt";
logger.AddEventRecord(this);
CategoryVM = new CheckItemVM<string>(
new List<string>() {
"01 elem",
"02 elem",
"03 elem",
"04 elem",
"05 elem",
"06 elem",
"07 elem",
"08 elem",
"09 elem",
"10 elem",
"11 elem",
"12 elem",
"13 elem",
"14 elem",
"15 elem",
"16 elem",
"17 elem",
"18 elem",
"19 elem",
"20 elem",
"21 elem",
"22 elem",
"23 elem",
"24 elem",
"25 elem"});
}
public ICommand SelectAll
{
get
{
return new RelayCommand
{
ExecuteAction = a =>
{
CategoryVM.SelectAll();
},
CanExecutePredicate = p =>
{
return !CategoryVM.IsAllSelected();
}
};
}
}
public ICommand InvertSelection
{
get
{
return new RelayCommand
{
ExecuteAction = a =>
{
CategoryVM.InvertSelection();
},
CanExecutePredicate = p =>
{
return true;
}
};
}
}
public ICommand ClearAll
{
get
{
return new RelayCommand
{
ExecuteAction = a =>
{
CategoryVM.ClearAll();
},
CanExecutePredicate = p =>
{
return CategoryVM.Checked.Any();
}
};
}
}
class CheckItemVM<T>
{
protected ObservableCollection<CheckItemPresenterVM<T>> _checked = new ObservableCollection<CheckItemPresenterVM<T>>();
public ObservableCollection<CheckItemPresenterVM<T>> Source { get; set; }
public ObservableCollection<CheckItemPresenterVM<T>> Checked { get => _checked; }
public CheckItemVM(ICollection<T> _source)
{
Source = new ObservableCollection<CheckItemPresenterVM<T>>();
UpdateSource(_source);
}
protected void UpdateSource(ICollection<T> _source)
{
foreach (var item in _source)
{
Source.Add(new CheckItemPresenterVM<T>(ref _checked)
{ Item = item, IsChecked = false });
}
}
public void SetSelection(CheckItemPresenterVM<T> sourceItem, bool flag)
{
if (sourceItem.IsChecked != flag)
{
sourceItem.IsChecked = flag;
}
}
public void SelectAll()
{
foreach (var item in Source)
{
item.IsChecked = true;
}
}
public void ClearAll()
{
foreach (var item in Source)
{
item.IsChecked = false;
}
}
public void InvertSelection()
{
foreach (var item in Source)
{
if (item.IsChecked) item.IsChecked = false;
else item.IsChecked = true;
}
}
public bool IsAllSelected()
{
return Source.Count == Checked.Count;
}
}
class CheckItemPresenterVM<T> : INotifyPropertyChanged
{
protected bool _isChecked;
protected ObservableCollection<CheckItemPresenterVM<T>> _checked;
public CheckItemPresenterVM(ref ObservableCollection<CheckItemPresenterVM<T>> Checked)
{
_checked = Checked;
}
public T Item { get; set; }
public string Name { get; set; }
public bool IsChecked
{
get
{
return _isChecked;
}
set
{
_isChecked = value;
if (value)
{
if (!_checked.Contains(this))
{
_checked.Add(this);
NotifyPropertyChanged("Item was Checked");
}
}
else
{
if (_checked.Contains(this))
{
_checked.Remove(this);
NotifyPropertyChanged("Item Unchecked");
}
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public override string ToString()
{
return Item.ToString();
}
}
I see strange part of you code.
Guessing this fix in PropertyChanged raising call
public bool IsChecked
{
get
{
return _isChecked;
}
set
{
_isChecked = value;
if (value)
{
if (!_checked.Contains(this))
{
_checked.Add(this);
NotifyPropertyChanged("IsChecked");
}
}
else
{
if (_checked.Contains(this))
{
_checked.Remove(this);
NotifyPropertyChanged("IsChecked");
}
}
}
}
NotifyPropertyChanged fires PoropertyChanged Event with a string parameter you passing to. Meanwhile Binding will receive it.
So, here's 2 friends
<Setter Property="IsSelected" Value="{Binding IsChecked, Mode=TwoWay}" />
and
NotifyPropertyChanged("IsChecked");
If you need tune the update behavior of your control, pass UpdateSourceTrigger setting to to the binding, this way:
<Setter Property="IsSelected" Value="{Binding IsChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
Additionally: TwoWay is default for the Mode here. You may not declare it.
When binding a View/Control to an ObservableCollection the View/Control will be redrawn when the CollectionChanged event of ObservableCollection is raised.
This occurs when an item is added, removed, reordered or assigned a new instance; but not (as you've probably realised) when an item raises it's PropertyChanged event.
To get your changes to be reflected in the UI you need the PropertyChanged of each Item to raise the CollectionChanged event of the collection.
public class ObservableItemCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
// Call this from the constructor
private void InitialiseItems()
{
CollectionChanged += ContentCollectionChanged;
foreach (T item in Items)
item.PropertyChanged += ReplaceElementWithItself;
}
private void ContentCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
foreach (T item in e.OldItems)
{
item.PropertyChanged -= ReplaceElementWithItself;
}
}
if (e.NewItems != null)
{
foreach (T item in e.NewItems)
{
item.PropertyChanged += ReplaceElementWithItself;
}
}
}
private void ReplaceElementWithItself(object sender, PropertyChangedEventArgs e)
{
var collectionChangedArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender, IndexOf((T)sender));
// Call this on the main thread
OnCollectionChanged(collectionChangedArgs);
}
}
This implementation simply raises the CollectionChanged event as a 'Replace' where the same Item is both removed and inserted, at the same index. Beware that it could have unintended consequences if you have anything that actually cares about the CollectionChanged events beyond binding it to the UI; but it is the simplest way I have found to achieve what you are after.
I've added a ScrollViewer behavior that allows me to scroll to the top when a property is set to true based on Scroll the scrollviewer to top through viewmodel. I've found that this works perfectly the first time, but subsequent attempts do not fire because the TwoWay binding isn't setting my property back to false.
Here is my simple project showing my issue:
MainWindow.xaml
<Window x:Class="WpfApp1.MainWindow"
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"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="100" Width="200">
<ScrollViewer local:ScrollViewerBehavior.ScrollToTop="{Binding Reset, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="200"/>
</Grid.RowDefinitions>
<Button VerticalAlignment="Bottom" Content="Top" Command="{Binding Scroll}"/>
</Grid>
</ScrollViewer>
</Window>
MainWindow.xaml.cs
using System.Windows;
namespace WpfApp1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private ViewModel _ViewModel;
public MainWindow()
{
InitializeComponent();
DataContext = _ViewModel = new ViewModel();
}
}
}
ViewModel.cs
using System;
using System.ComponentModel;
using System.Windows.Input;
namespace WpfApp1
{
public class RelayCommand : ICommand
{
private readonly Action<object> action;
public RelayCommand(Action<object> action)
{
this.action = action;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
action(parameter);
}
}
public class ViewModel : INotifyPropertyChanged
{
private bool _reset = false;
public ViewModel()
{
Scroll = new RelayCommand(o =>
{
Reset = true;
});
}
public event PropertyChangedEventHandler PropertyChanged;
public bool Reset
{
get { return _reset; }
set
{
bool changed = value != _reset;
_reset = value;
if (changed)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Reset)));
}
}
}
public RelayCommand Scroll { get; set; }
}
}
ScrollViewerBehavior.cs
using System.Windows;
using System.Windows.Controls;
namespace WpfApp1
{
public static class ScrollViewerBehavior
{
public static readonly DependencyProperty ScrollToTopProperty = DependencyProperty.RegisterAttached("ScrollToTop", typeof(bool), typeof(ScrollViewerBehavior), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (o, e) =>
{
if (o is ScrollViewer sc)
{
if ((bool)e.NewValue)
{
sc.ScrollToTop();
SetScrollToTop((FrameworkElement)o, false); // this should set the property back to false
}
}
}));
public static bool GetScrollToTop(FrameworkElement o)
{
return (bool)o.GetValue(ScrollToTopProperty);
}
public static void SetScrollToTop(FrameworkElement o, bool value)
{
o.SetValue(ScrollToTopProperty, value);
}
}
}
I know that if I take out the changed check on the property, it works; however, that is not ideal for my situation. When I look at the element through WPF Inspector, I see that the property on the ScrollViewer is false as it should be, but my ViewModel property remains true.
I'm not good in English but i wrote this example. Look at this
[MainWindow.xaml.cs]
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
AnyClass c = new AnyClass();
private void h1_Click(object sender, RoutedEventArgs e)
{
Test = true;
}
private void h2_Click(object sender, RoutedEventArgs e)
{
Test = false;
}
public bool Test
{
get { return (bool)GetValue(TestProperty); }
set
{
SetValue(TestProperty, value);
c.Val = value;
}
}
public static readonly DependencyProperty TestProperty =
DependencyProperty.Register("Test", typeof(bool), typeof(MainWindow), new PropertyMetadata(false));
}
[AnyClass.cs]
class AnyClass
{
private bool val = false;
public bool Val
{
get
{
return val;
}
set
{
val = value;
}
}
}
[mainWindow.xaml]
<Button Click="h1_Click" Content="true">
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=hUserControl, Path=Test}" Value="True">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<DataTrigger Binding="{Binding ElementName=hUserControl, Path=Test}" Value="False">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button Click="h2_Click" Content="false">
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=hUserControl, Path=Test}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding ElementName=hUserControl, Path=Test}" Value="False">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
I am trying to create a File Explorer style TreeView/ListView window that will allow the user to select a "Project" from within a "ProjectFolder".
The "ProjectFolder" tree is bound to a property on my ViewModel called "RootProjectFolders" and the "Project" list is bound to a property called "ProjectsInSelectedFolder". Things were mostly working; however, I was getting null exceptions when I first loaded the window because the "SelectedFolder" had not yet been set. When I tried to implement a simple check to make sure that the "SelectedFolder" was not null, my "Project" ListView stopped refreshing.
if ((this.SelectedFolder != null) && (this.SelectedFolder.ProjectFolder.Projects != null))
{
foreach (Project project in this.SelectedFolder.ProjectFolder.Projects)
{
_projectsInSelectedFolder.Add(new ProjectViewModel(project));
}
}
base.RaisePropertyChangedEvent("ProjectsInSelectedFolder");
If I remove (this.SelectedFolder != null) from the above, the ListView will update, but I will get an NullException error. Why is that check breaking my binding?
Following up on the request for additional information, here is the XAML of the TreeView and ListView that are binding to the properties on the ViewModel:
<TreeView Name="treeviewProjectFolders" Grid.Column="0"
ItemsSource="{Binding Path=RootProjectFolders}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate
ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Path=Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<GridSplitter Name="splitterProjects" Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
<ListView Name="listviewProjects" Grid.Column="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
ItemsSource="{Binding Path=ProjectsInSelectedFolder}">
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
And here is the ViewModel
public class SelectProjectViewModel : ViewModelBase
{
#region Fields
List<ProjectViewModel> _projectsInSelectedFolder;
List<ProjectFolderViewModel> _rootProjectFolders;
static ProjectFolderViewModel _selectedFolder = null;
ProjectViewModel _selectedProject;
#endregion // Fields
#region Constructor
public SelectProjectViewModel(ProjectFolders rootProjectFolders)
{
if (_rootProjectFolders != null) { _rootProjectFolders.Clear(); }
_rootProjectFolders = new List<ProjectFolderViewModel>();
foreach (ProjectFolder rootFolder in rootProjectFolders)
{
_rootProjectFolders.Add(new ProjectFolderViewModel(rootFolder, this));
}
_projectsInSelectedFolder = new List<ProjectViewModel>();
// Subscribe to events
this.PropertyChanged += OnPropertyChanged;
}
#endregion // Constructor
#region Properties
public List<ProjectFolderViewModel> RootProjectFolders
{
get
{
return _rootProjectFolders;
}
}
public List<ProjectViewModel> ProjectsInSelectedFolder
{
get
{
return _projectsInSelectedFolder;
}
}
public ProjectFolderViewModel SelectedFolder
{
get
{
return _selectedFolder;
}
set
{
if (_selectedFolder != value)
{
_selectedFolder = value;
}
}
}
public ProjectViewModel SelectedProject
{
get
{
return _selectedProject;
}
set
{
_selectedProject = value;
base.RaisePropertyChangedEvent("SelectedProject");
}
}
#endregion // Properties
#region Methods
public void FindSelectedFolder(ProjectFolderViewModel root)
{
if (root.IsSelected) { _selectedFolder = root; }
else
{
foreach (ProjectFolderViewModel folder in root.Children)
{
if (_selectedFolder == null)
{
FindSelectedFolder(folder);
}
}
}
}
#endregion // Methods
#region Event Handlers
void OnPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "SelectedFolder":
_selectedFolder = null;
foreach (ProjectFolderViewModel root in this.RootProjectFolders)
{
if (_selectedFolder == null)
{
this.FindSelectedFolder(root);
}
}
_projectsInSelectedFolder.Clear();
if ((this.SelectedFolder != null) && (this.SelectedFolder.ProjectFolder.Projects != null))
{
foreach (Project project in this.SelectedFolder.ProjectFolder.Projects)
{
_projectsInSelectedFolder.Add(new ProjectViewModel(project));
}
}
base.RaisePropertyChangedEvent("ProjectsInSelectedFolder");
break;
}
}
#endregion // Event Handlers
Also, here is the ViewModel for the individual project folders that are used to raise the "SelectedFolder" property:
public class ProjectFolderViewModel : ViewModelBase
{
#region Fields
ReadOnlyCollection<ProjectFolderViewModel> _children;
List<ProjectFolderViewModel> _childrenList;
bool _isExpanded;
bool _isSelected;
ProjectFolderViewModel _parentNode;
SelectProjectViewModel _parentTree;
ProjectFolder _projectFolder;
#endregion // Fields
#region Constructor
public ProjectFolderViewModel(ProjectFolder projectFolder, SelectProjectViewModel parentTree) : this(projectFolder, parentTree, null)
{ }
private ProjectFolderViewModel(ProjectFolder projectFolder, SelectProjectViewModel parentTree, ProjectFolderViewModel parentNode)
{
_projectFolder = projectFolder;
_parentTree = parentTree;
_parentNode = parentNode;
_childrenList = new List<ProjectFolderViewModel>();
foreach (ProjectFolder child in _projectFolder.ChildFolders)
{
_childrenList.Add(new ProjectFolderViewModel(child, _parentTree));
}
_children = new ReadOnlyCollection<ProjectFolderViewModel>(_childrenList);
}
#endregion // Constructor
#region Properties
public ReadOnlyCollection<ProjectFolderViewModel> Children
{
get
{
return _children;
}
}
public bool IsExpanded
{
get
{
return _isExpanded;
}
set
{
if (value != _isExpanded)
{
_isExpanded = value;
this.OnPropertyChanged("IsExpanded");
}
// Expand all the way up to the root.
if (_isExpanded && _parentNode != null)
_parentNode.IsExpanded = true;
}
}
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
_isSelected = value;
base.RaisePropertyChangedEvent("IsSelected");
//if (_isSelected)
//{
_parentTree.RaisePropertyChangedEvent("SelectedFolder");
//}
}
}
public string Name
{
get
{
return _projectFolder.Name;
}
}
public ProjectFolder ProjectFolder
{
get
{
return _projectFolder;
}
}
#endregion // Properties
Change all your
List<T> to observablecollection<T>
because when ever there is new file or folder your adding the Item, your not creating new List, since observablecollection implements INotifyCollectionChanged, and INotifyPropertyChanged it'll internally take care of notifying and refreshing the View. But list cant do that