I'm working on getting a UserControl in WPF working that has a MenuItem populated with an ItemsSource, which creates a menu that goes n levels deep (although I'm just looking at TopMenuItem\Branches\Leaves right now).
The wrinkle I'm having trouble with is that I want to filter the leaves through a textbox embedded into the menu. If a branch has no leaves, it also gets filtered out. It looks like this at the moment :
I'm working with an ObservableCollection of IMenuTreeItem, which can contain branches (which in turn also has an ObservableCollection of IMenuTreeItem) or leaves.
public interface IMenuTreeItem
{
string Name { get; set; }
}
public class MenuTreeLeaf : IMenuTreeItem
{
public string Name { get; set; }
public Guid UID { get; set; }
public ObjectType Type { get; set; }
public Requirement Requirement { get; set; }
public MenuTreeLeaf(string name, ObjectType type, Guid uID)
{
Type = type;
Name = name;
UID = uID;
}
public MenuTreeLeaf(string name)
{
Name = name;
}
}
public class MenuTreeBranch : IMenuTreeItem, INotifyPropertyChanged
{
public string Name { get; set; }
private ObservableCollection<IMenuTreeItem> _items;
public ObservableCollection<IMenuTreeItem> Items
{
get
{
return _items;
}
set
{
_items = value; OnPropertyChanged();
}
}
public MenuTreeBranch(string name)
{
Name = name;
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
This is how I'm filtering. It very much feels like there's a better way.
ObservableCollection<IMenuTreeItem> result = new ObservableCollection<IMenuTreeItem>(ItemsSource);
for (int i = 0; i < result.Count; i++)
{
if (result[i] is MenuTreeBranch currentBranch)
{
if (currentBranch.Items != null)
currentBranch.Items = new ObservableCollection<IMenuTreeItem>(currentBranch.Items.Where(x => x.Name.ToLower().Contains(SearchField.ToLower())));
}
}
result = new ObservableCollection<IMenuTreeItem>(result.Where(x => (x as MenuTreeBranch).Items.Count > 0));
result.Insert(0, new MenuTreeLeaf("[Search]"));
return result;
So my main problems are:
When I've filtered, I can no longer unfilter. ItemsSource gets changed too. Could it be because I'm filtering in the ItemsSourceFiltered getter? I tried to clone, but eh, didn't change anything
When I call OnPropertyChanged on ItemsSourceFiltered any time text changes in the textbox, the menu closes. The menu definitely shouldn't close while you're inputting text.
Any advice?
You may have a menu item class that exposes a recursive Filter string and a collection property that returns the filtered child items:
public class FilteredMenuItem : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string Name { get; set; }
public ICommand Command { get; set; }
private string filter;
public string Filter
{
get { return filter; }
set
{
filter = value;
foreach (var childItem in ChildItems)
{
childItem.Filter = filter;
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Filter)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FilteredChildItems)));
}
}
public List<FilteredMenuItem> ChildItems { get; set; } = new List<FilteredMenuItem>();
public IEnumerable<FilteredMenuItem> FilteredChildItems
{
get { return string.IsNullOrEmpty(Filter)
? ChildItems
: ChildItems.Where(childItem => (bool)childItem.Name?.Contains(Filter)); }
}
}
With a RootItem property in the view model like
public FilteredMenuItem RootItem { get; }
= new FilteredMenuItem { Name = "Items" };
you may bind to it in XAML like this:
<StackPanel DataContext="{Binding RootItem}">
<TextBox Text="{Binding Filter, UpdateSourceTrigger=PropertyChanged}"/>
<Menu>
<Menu.Resources>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Name}"/>
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="ItemsSource"
Value="{Binding FilteredChildItems}"/>
</Style>
</Menu.Resources>
<MenuItem/>
</Menu>
</StackPanel>
While you populate the ChildItems property of each FilteredMenuItem, the view only shows the FilteredChildItems collection.
You may also notice that the above doesn't use ObservableCollection at all, since no items are added to or removed from any collection at runtime. You just have to make sure the item tree is populated before the view is loaded.
Related
I have a combobox that has an items source of type ObservableCollection<Clinic>
<ComboBox ItemsSource="{Binding Source={StaticResource ClinicList}}" DisplayMemberPath="Name" SelectedValue="{Binding Path=Name}" SelectedValuePath="Name"></ComboBox>
This combobox is within a ListView that is bound from EmployeeClinics.
public class Employee{
public ObservableCollection<Clinic> EmployeeClinics { get; set; }
}
When I launch the app I see the appropriate clinics. And the drop down seems to show the correct options, but when I update them, only the Name updates and not the ClinicId (it keeps previous ClinicId).
Edit: Similarly when I add a new clinic to the list and select it from the options, it's Id is 0 when I look at the collection.
Here is my clinic model.
public class Clinic {
public int ClinicId { get; set; }
public string _name { get; set; }
public string Name {
get {
return _name;}
set {
if (_name != value) {
_name = value;
}
}
}
}
UPDATE: Thanks #AyyappanSubramanian. I am making headway. I have updated my Objects
public class Employee{
public ObservableCollection<ClinicView> EmployeeClinics { get; set; }
}
public class ClinicView {
private Clinic selectedClinic;
public Clinic SelectedClinic {
get { return selectedClinic; }
set {
selectedClinic = value;
selectedClinicId = selectedClinic.ClinicId;
}
}
private int selectedClinicId;
public int SelectedClinicId {
get { return selectedClinicId; }
}
}
XAML:
<ComboBox ItemsSource="{Binding Source={StaticResource ClinicList}}" DisplayMemberPath="Name" SelectedItem="{Binding SelectedClinic}"></ComboBox>
Changing the drop downs now properly changes the underlying object and updates the list as desired. Now my only issue is that the comboboxes don't display the current object, just show as blank on start. I've messed around with SelectedValue and Path with no luck. Any suggestions?
Refer the below code. You can use SelectedItem to get both the ID and Name in one SelectedObject. Get only ID using SelectedValue.
<ComboBox ItemsSource="{Binding Clinics}" DisplayMemberPath="ClinicName"
SelectedValuePath="ClinicId" SelectedValue="{Binding SelectedClinicId}"
SelectedItem="{Binding SelectedClinic}"/>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
}
class Clinic
{
public int ClinicId { get; set; }
public string ClinicName { get; set; }
}
class ViewModel
{
public ObservableCollection<Clinic> Clinics { get; set; }
public ViewModel()
{
Clinics = new ObservableCollection<Clinic>();
for (int i = 0; i < 10; i++)
{
Clinics.Add(new Clinic() { ClinicId=i+1,ClinicName="MyClinic"+(i+1) });
}
}
private int selectedClinicId;
public int SelectedClinicId
{
get { return selectedClinicId; }
set
{
selectedClinicId = value;
}
}
private Clinic selectedClinic;
public Clinic SelectedClinic
{
get { return selectedClinic; }
set
{
selectedClinic = value;
MessageBox.Show("ID:"+selectedClinic.ClinicId.ToString()+" "+"Name:"+selectedClinic.ClinicName);
}
}
}
I have datagrid with two columns: text and combobox. And combobox should have binding to observable collection.
This is pseudocode for datagrid items source:
public class ModeObjectState
{
public int ID { get; set; }
public int ObjectTypeID { get; set; }
public string State { get; set; }
}
public class ModeObject
{
public string Name { get; set; }
public int objID { get; set; }
public int Type { get; set; }
public int StateID { get; set; }
public bool Format { get; set; }
}
public class _dataContext
{
public ObservableCollection<ModeObjectState> ListObjectState { get; set; }
public ModeObject ModeObj { get; set; }
}
ObservableCollection<_dataContext> SourceObjList
objTable.ItemsSource = SourceObjList;
This is xaml code for datagrid:
<DataGrid x:Name="objTable" AutoGenerateColumns="False" >
<DataGrid.Columns>
<DataGridTextColumn x:Name="ColumnName" Binding="{Binding Path=ModeObj.Name}" IsReadOnly="True" />
<DataGridComboBoxColumn x:Name="ColumnState" ItemsSource="{Binding ListObjectState}" DisplayMemberPath="State" SelectedValuePath="ID" SelectedValueBinding="{Binding Path=ModeObj.StateID}" />
</DataGrid.Columns>
</DataGrid>
But datagrid doesn't show any items in comboboxcolumn. Please, help me with binding the datagridcombobox to observable collection "ListObjectState" in "_dataContext" class.
Thanks!
Implement with INotifyPropertyChanged for the _dataContext
public class _dataContext : INotifyPropertyChanged
{
private ObservableCollection<ModeObjectState> _listObjectState;
public ObservableCollection<ModeObjectState> ListObjectState
{
get { return _listObjectState; }
set
{
_listObjectState = value;
OnPropertyChagned("ListObjectState");
}
}
public ModeObject ModeObj { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChagned(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Since the 1st set may done after the Binding, so it wont affect the UI..
Its difficult to figure out without looking at the whole code. You have binding issue, and it would be easier to find where the issue is using XAML debugging tools like Snoop or WPF Inspector. You just need to attach your running application to see the Datacontext.
You can easily find if the datacontext is valid or not.
WPF Inspector has a better User interface, but its prone to crash. Press Ctrl+Shift and hover mouse over your control to see it getting reflected in Snoop/WPF Inspected.
Also see your Output window for whats the binding error you are getting.
There are several articles written about similar tasks to this. However, none seem to be close enough to work for what I am doing. I have a custom control that has a ListBox in its template. I have reworked the template for the ListBox to my liking. When an item is selected, I want to change the color. Here's the part where my problem seems to diverge to most others: I don't know what color. It is whatever color is in the item being rendered. I bound the different colors in XAML, but it doesn't redraw when I set a new color. I have changed the default color in the items to make sure that the template was picking up the right values in the first place. That succeeded. Things I have tried: binding, having the items implement INotifyPropertyChanged, and EventTrigger with Storyboard (which never really built I assume because my value wasn't a static resource). I am missing something very basic here. I'm sure. Here are code excerpts to help:
XAML:
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<Border BorderBrush="{Binding CurrentState.Border}" BorderThickness="1">
<TextBlock Text="{Binding DisplayObject}" Foreground="{Binding CurrentState.Foreground}" Background="{Binding CurrentState.Background}" MinHeight="12" MinWidth="50" Padding="2" ToolTip="{Binding ToolTip}"/>
</Border>
</DataTemplate>
</Setter.Value>
</Setter>
Helper classes:
public class MultiStateSelectionGridState
{
public string Background { get; set; }
public string Foreground { get; set; }
public string Border { get; set; }
public string Text { get; set; }
public MultiStateSelectionGridState()
{
Background = "White";
Foreground = "Black";
Border = "Black";
Text = String.Empty;
}
};
public interface IMultiStateSelectionGridItem : INotifyPropertyChanged
{
object DisplayObject { get; }
object ToolTip { get; }
object Value { get; }
MultiStateSelectionGridState CurrentState { get; set; }
void OnPropertyChanged(PropertyChangedEventArgs e);
};
I don't know how much of the item class I can post, so I will not do so initially. It looks like the following though:
class SomeItem : IMultiStateSelectionGridItem
{
public int SomeInt { get; set; }
public string SomeString { get; set; }
public string SomeOtherString { get; set; }
public object DisplayObject
{
get { return SomeString + CurrentState.Text; }
}
public object ToolTip
{
get { return SomeOtherString; }
}
public object Value
{
get { return SomeInt; }
}
private MultiStateSelectionGridState m_currentState;
public MultiStateSelectionGridState CurrentState
{
get
{
return m_currentState;
}
set
{
m_currentState = value;
//Notice that this was just test code and I tried CurrentState, Background, and what
//you see there now.
OnPropertyChanged(new PropertyChangedEventArgs("CurrentState.Background"));
}
}
public SomeItem()
{
SomeInt = 0;
SomeString = String.Empty;
SomeOtherString = String.Empty;
CurrentState = new MultiStateSelectionGridState();
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
};
Any help would be much appreciated.
Ok your implementing INPC in SomeItem and not MultiStateSelectionGridState so when you switch state you need to pretty much create a new CurrentState object than go something like CurrentState.Background = "Blue"; in code-behind
You also need to switch
OnPropertyChanged(new PropertyChangedEventArgs("CurrentState.Background"));
to
OnPropertyChanged(new PropertyChangedEventArgs("CurrentState"));
Now when you switch the CurrentState variable(when item is selected) it will propagate to the View and it's properties will be queried accordingly.
I tested this by doing something like:
Items = new ObservableCollection<SomeItem> {
new SomeItem {
CurrentState = new MultiStateSelectionGridState()
}
};
// Simulating a Selected State change
var tempTask = new Task
(
() => {
Thread.Sleep(5000);
Items[0].CurrentState = new MultiStateSelectionGridState {
Background = "Green",
Border = "Blue"
};
},
TaskCreationOptions.LongRunning
);
tempTask.Start();
and having ListBox have it's ItemsSource as Items from above.
You can find a working example of this Here
If you do not want to keep re-creating the CurrentState object for state changes, make
MultiStateSelectionGridState implement INPC itself.
Side-note
I do not know where or when you actually get to know what Colors to set for the control, so cant advise how to move that to xaml. But you should look to having these come from xaml
The first code section below is the code I am tempted to write. The second code section below is what a previous code worker wrote when trying to achieve the same task.
The previous co-worker's code seems to follow standard MVVM practice on having a seperate ViewModel for each type of item, of keeping track of SelectedItems in the ViewModel rather than the view, and of avoiding ObservableCollection in the model.
The code I am tempted to write is about half the size and complexity, with less potential for the model and ViewModel getting out of sync, and far less lines of code.
Is MVVM best practice really the right answer here? Is there some sort of middle ground combining the best of both versions?
My code:
//Model
public class Cheese
{
public string Name { get; set; }
public int Tastiness { get; set; }
public Color Color { get; set; }
}
public class CheeseEditorModel
{
public ObservableCollection<Cheese> Cheeses { get; private set; }
public CheeseEditorModel()
{
//read cheeses in from file/database/whatever
}
public DeleteCheeses(SelectedObjectCollection selected)
{
//delete cheeses
}
}
//ViewModel
public class CheeseEditorViewModel
{
private CheeseEditorModel _model;
public ObservableCollection<Cheese> Cheeses { get {return _model.Cheeses} }
public CheeseEditorViewModel()
{
_model = new CheeseEditorModel();
}
public DeleteSelected(SelectedObjectCollection selected)
{
_model.Delete(selected);
}
}
//XAML
<ListBox Name="CheeseListBox" ItemsSource={Binding Path="Cheeses"} />
<Button Command={Binding DeleteSelected} CommandParameter="{Binding ElementName=CheeseListBox, Path=SelectedItems}" />
Other person's code:
//Model
public class Cheese
{
public string Name { get; set; }
public int Tastiness { get; set; }
public Color Color { get; set; }
}
public class CheeseEditorModel
{
public List<Cheese> Cheeses { get; private set; }
public CheeseDataModel()
{
//read cheeses in from file/database/whatever
}
public DeleteCheeses(IEnumerable<Cheese> toDelete)
{
//delete cheeses
}
}
//ViewModel
public class CheeseViewModel
{
private Cheese _cheese { get; set; }
public bool IsSelected { get; set; }
public CheeseViewModel(Cheese cheese)
{
_cheese = cheese;
IsSelected = false;
}
public string Name {get {return _cheese.Name} set { _cheese.Name = value } }
public int Tastiness {get {return _cheese.Tastiness} set { _cheese.Tastiness= value } }
public Color Color {get {return _cheese.Color} set { _cheese.Color = value } }
}
public class CheeseEditorViewModel
{
private CheeseEditorModel _model;
public ObservableCollection<CheeseViewModel> Cheeses { get; private set; }
public CheeseEditorViewModel()
{
_model = new CheeseEditorModel();
foreach (cheese in _model.Cheeses)
Cheeses.Add(cheese);
}
public DeleteSelected()
{
var selected = from cheese in Cheeses select cheese.CheeseModel where cheese.IsSelected();
_model.Delete(selected);
var selectedVM = from cheese in Cheeses select cheese where cheese.IsSelected();
foreach (cheese in selectedVM)
Cheeses.Remove(selected);
}
}
//XAML
<ListBox ItemsSource={Binding Path="Cheeses"}>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding Mode=TwoWay, Path=IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
<Button Command={Binding DeleteSelected} />
does Cheese implement INotifyPropertyChanged? if yes why? but nevertheless i go with the other's person code. you can easily add new non model related stuff
I have the Xaml which should basically bind a set of ContextualButtons for a selected tab's viewmodel to the ItemsSource property of the ToolBar. For some reason, this binding is not actually occuring unless I use Snoop to inspect the element manually...It seems that the act of snooping the element is somehow requerying the binding somehow.
Does anyone know what I might be doing wrong here? This behavior is the same if I use a Listbox as well, so I know it is something that I am doing incorrectly...but I am not sure what.
SelectedView is a bound property to the selected view from a Xam Tab control.
XAML
<ToolBar DataContext="{Binding SelectedView.ViewModel}"
ItemsSource="{Binding ContextualButtons}" >
<ToolBar.ItemTemplate>
<DataTemplate>
<!-- <Button ToolTip="{Binding Name}"-->
<!-- Command="{Binding Command}">-->
<!-- <Button.Content>-->
<!-- <Image Width="32" Height="32" Source="{Binding ImageSource}"/>-->
<!-- </Button.Content>-->
<!-- </Button>-->
<Button Content="{Binding Name}"/>
</DataTemplate>
</ToolBar.ItemTemplate>
</ToolBar>
Code
public class TestViewModel : BaseViewModel, IBulkToolViewModel
{
public TestViewModel()
{
ContextualButtons = new ObservableCollection<IContextualButton>()
{
new ContextualButton("Test Button",
new DelegateCommand<object>(
o_ => Trace.WriteLine("Called Test Button")), String.Empty)
};
}
public string Key { get; set; }
private ObservableCollection<IContextualButton> _contextualButtons;
public ObservableCollection<IContextualButton> ContextualButtons
{
get { return _contextualButtons; }
set
{
if (_contextualButtons == value) return;
_contextualButtons = value;
//OnPropertyChanged("ContextualButtons");
}
}
}
public partial class TestView : UserControl, IBulkToolView
{
public TestView()
{
InitializeComponent();
}
public IBulkToolViewModel ViewModel { get; set; }
}
public class ContextualButton : IContextualButton
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
public ICommand Command { get; set; }
public string ImageSource { get; set; }
public ContextualButton(string name_, ICommand command_, string imageSource_)
{
Name = name_;
Command = command_;
ImageSource = imageSource_;
}
}
public class BulkToolShellViewModel : BaseViewModel, IBaseToolShellViewModel, IViewModel
{
private IBulkToolView _selectedView;
public IBulkToolView SelectedView
{
get
{
return _selectedView;
}
set
{
if (_selectedView == value) return;
_selectedView = value;
OnPropertyChanged("SelectedView");
}
}
public ObservableCollection<IBulkToolView> Views { get; set; }
public DelegateCommand<object> AddViewCommand { get; private set; }
public DelegateCommand<object> OpenPortfolioCommand { get; private set; }
public DelegateCommand<object> SavePortfolioCommand { get; private set; }
public DelegateCommand<object> GetHelpCommand { get; private set; }
public BulkToolShellViewModel(ObservableCollection<IBulkToolView> views_)
: this()
{
Views = views_;
}
public BulkToolShellViewModel()
{
Views = new ObservableCollection<IBulkToolView>();
AddViewCommand = new DelegateCommand<object>(o_ => Views.Add(new TestView
{
ViewModel = new TestViewModel()
}));
OpenPortfolioCommand = new DelegateCommand<object>(OpenPortfolio);
SavePortfolioCommand = new DelegateCommand<object>(SavePortfolio);
GetHelpCommand = new DelegateCommand<object>(GetHelp);
}
private void GetHelp(object obj_)
{
}
private void SavePortfolio(object obj_)
{
}
private void OpenPortfolio(object obj_)
{
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged()
{
throw new NotImplementedException();
}
public void RaisePropertyChanged(string propertyName)
{
throw new NotImplementedException();
}
public string this[string columnName]
{
get { throw new NotImplementedException(); }
}
public string Error { get; private set; }
public AsyncContext Async { get; private set; }
public XmlLanguage Language { get; private set; }
public string Key { get; set; }
}
Thanks!
Why does BulkToolShellViewModel have its own PropertyChanged event along with RaisePropertyChanged methods that do nothing? Shouldn't it inherit this functionality from BaseViewModel? Perhaps the UI is attaching to BulkToolShellViewModel.PropertyChanged rather than BaseViewModel.PropertyChanged and is never being notified of changes.