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
Related
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.
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'm binding an editable (you can type in it to add items to the list of choices) radcombobox in a column of a radgridview. It is not throwing a binding error, but it is not updating the bound property (Model.Remarks)
Here are the classes
public class NotamRemarkList : List<string>
{
public NotamRemarkList()
{
Add("Precision approaches are down; higher weather minimums apply.");
Add("Due to runway closure, approaches available have higher minimums.");
Add("All approaches are down; weather must be VFR.");
Add("Long runway is closed; issue if the other runways are wet.");
Add("Runway shortened; issue if wet.");
Add("Airport will be closed at the time we are scheduled in.");
Add("Runway lights are inoperative; night flights prohibited.");
}
}
public class NotamViewModel
{
[DataMember]
public string NewStatus { get; set; }
[DataMember]
public Notam Model { get; set; }
[DataMember]
public string NotamGroup { get; set; }
[DataMember]
public int NotamCount { get; set; }
[DataMember]
public DateTime? EarliestNotamDepartureTime { get; set; } // min_dep_datetime
[DataMember]
public string RadioButtonGroupName { get; set; }
}
public class Notam
{
[DataMember]
public string Remarks { get; set; }
[DataMember]
public string TripNumber { get; set; }
[DataMember]
public string ArrivalDeparture { get; set; }
}
Here's the xaml I have tried for the column - the first one uses a cell template, the second attempts to do everything in a column
<telerik:GridViewDataColumn Header="Remarks" IsFilterable="False" IsSortable="False" IsReadOnly="False" Width="430">
<telerik:GridViewDataColumn.CellTemplate>
<DataTemplate>
<telerik:RadComboBox SelectedValue="{Binding Model.Remarks, Mode=TwoWay}" ItemsSource="{StaticResource NotamRemarkList}" IsEditable="True"/>
</DataTemplate>
</telerik:GridViewDataColumn.CellTemplate>
</telerik:GridViewDataColumn>
<telerik:GridViewComboBoxColumn SelectedValueMemberPath="Model.Remarks" UniqueName="colRemarks" IsComboBoxEditable="true" IsFilterable="False" IsSortable="False"/>
public class Notam:INotifyPropertyChanged
{
private string _remarks;
[DataMember]
public string Remarks
{
get {return _remarks;}
set{
_remarks=value ;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Remarks"));
}
}
[DataMember]
public string TripNumber { get; set; }
[DataMember]
public string ArrivalDeparture { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
}
<telerik:RadComboBox SelectedValue="{Binding Model.Remarks, Mode=TwoWay}" SelectedValueMemberPath="Model.Remarks" ItemsSource="{StaticResource NotamRemarkList}" IsEditable="True"/>
I hope this will help.
I've been sitting on this problem for hours now,
I've got this partial xaml code:
<TextBlock Text="{Binding temprature}" Height="30" HorizontalAlignment="Left" Margin="13,119,0,0" Name="textBlock1" VerticalAlignment="Top" Width="68" />
<TextBlock Height="30" HorizontalAlignment="Left" Name="commentsTextBlock" Text="Comments:" VerticalAlignment="Bottom" Margin="12,0,0,-31" />
<ListBox Margin="2,785,-14,-33" ItemsSource="{Binding comments}" DataContext="{Binding}" Name="commentsListBox">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0,0,0,17">
<StackPanel Width="311">
<TextBlock Text="{Binding poster_username}" TextWrapping="NoWrap" Style="{StaticResource PhoneTextSubtleStyle}" TextTrimming="WordEllipsis" Width="Auto" Foreground="White" FontFamily="Segoe WP Semibold" />
<TextBlock Text="{Binding comment_text}" TextWrapping="Wrap" Margin="12,-6,12,0" Style="{StaticResource PhoneTextSubtleStyle}" TextTrimming="WordEllipsis" MaxHeight="100" />
</StackPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
And I have this class (Thread) which include a List of comments that should be displayed in the ListBox.
public class Thread : INotifyPropertyChanged
{
public string title { get; set; }
public string deal_link { get; set; }
public string mobile_deal_link { get; set; }
public string deal_image { get; set; }
public string deal_image_highres { get; set; }
public string description { get; set; }
public string submit_time { get; set; }
public bool hot_date { get; set; }
public string poster_name { get; set; }
public double temperature { get; set; }
public double price { get; set; }
public int timestamp { get; set; }
public string expired { get; set; }
public Forum forum { get; set; }
public Category category { get; set; }
public object merchant { get; set; }
public object tags { get; set; }
public int thread_id { get; set; }
public string visit_link { get; set; }
public string hot_time { get; set; }
public int comment_count { get; set; }
public string availability { get; set; }
public string can_vote { get; set; }
public string seen { get; set; }
public List<Comment> comments { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public void Convert2Unicode()
{
UnicodeEncoding unicode = new UnicodeEncoding();
Byte[] encodedBytes = unicode.GetBytes(title);
title = String.Format("[{0}]", title);
}
private void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public void SetComments(string content)
{
PaginatedComments commentsList;
this.comments.Clear();
DataContractJsonSerializer serializer =
new DataContractJsonSerializer(typeof(PaginatedComments));
using (MemoryStream ms = new MemoryStream(Encoding.Unicode.GetBytes(content)))
{
commentsList = (PaginatedComments)serializer.ReadObject(ms);
}
foreach (var thread in commentsList.data.comments)
{
this.comments.Add(thread);
}
}
public Thread()
{
comments = new List<Comment>();
}
public void addComments()
{
List<string> parameters = new List<string>();
parameters.Add("thread_id=" + thread_id);
parameters.Add("results_per_page=10");
parameters.Add("page=1");
// Set the data context of the listbox control to the sample data
APICalls.makeRequest(APICalls.ViewingPaginatedComments, parameters, SetComments);
}
//
// Sets the "Seen" variable depending if the item is new (since the last time the application was opened
//
public void updateNewItems()
{
try
{
int last_seen = (int)IsolatedStorageSettings.ApplicationSettings["lastrun"];
if (last_seen < timestamp)
{
seen = "New";
}
else
{
seen = "";
}
}
catch (System.Exception e)
{
IsolatedStorageSettings.ApplicationSettings["lastrun"] = APICalls.GetIntTimestamp();
IsolatedStorageSettings.ApplicationSettings.Save();
MessageBox.Show(e.Message);
}
IsolatedStorageSettings.ApplicationSettings["lastrun"] = APICalls.GetIntTimestamp();
IsolatedStorageSettings.ApplicationSettings.Save();
}
}
public class Comment
{
public string poster_username { get; set; }
public string post_date { get; set; }
public string comment_text { get; set; }
public int timestamp { get; set; }
public int like_count { get; set; }
public string comment_edit_text { get; set; }
}
public class CommentData
{
public List<Comment> comments { get; set; }
public int comment_count { get; set; }
public int total_comments { get; set; }
}
public class PaginatedComments
{
public string response_status { get; set; }
public CommentData data { get; set; }
}
If I load the comments into the Thread before changing the DataCotext to this specific thread. the comments are shown, but when I update the comments after changing the DataContext and navigating to the page the comments are not shown (I have other fields in the rest of the xaml page that are binded to the same instance and they work perfectly. only the comments don't work!
I really appreciate your help!
Thanks
You should be using
public ObservableCollection<Comment> comments{ get; set;}
instead of
public List<Comment> comments { get; set; }
The ObservableCollection sends update notices to the view anytime one of the items(in this case a Comment ) is added or removed.
Note: It won't update the Comment . To have the items bound to the Comment update, Comment has to implement INotifyPropertyChanged.
Your property is a simple List<T>. Silverlight needs to be signaled when something changes, using events.
A List<T> does not raise any event when items are being added/removed, so Silverlight cannot detect the new items and thus does not update the UI. The simplest way to make this work is usually to use an ObservableCollection<T> instead of a list.
This collection will raise events that Silverlight knows to listen to.
Please note that for this to work you should not call Add/Remove/Clear from any other thread than the U/I (Dispatcher) thread, since Silverlight controls are not thread-safe (and the events are raised on the thread that performs the Add/Remove/Clear call). To do that, simply make sure you call Dispatcher.BeginInvoke from your SetComments method (since it seems to be a callback happening from whatever your fetching mechanism is).
Alternatively, you could also regenerate a brand new List object when fetching comments, and raise the NotifyPropertyCHanged event in the SetComments method, but that would lead to losing the current selection and resetting your list to the top, which is probably not what you want to do.
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.