WPF Data Binding with multiple controls - c#

In WPF, I'm trying to bind multiple controls, but the second control isn't changing when the first control is changed.
I have two classes: a Task class, and a Log class, which is stored as a collection in the Task class. The list boxes below are bound to the Tasks, and the inner Logs for the selected Task.
The problem is that the list boxes are populated fine at first load, but if I select a different task, I'd expect the Logs to be update to the collection for the new Task, but it doesn't change from those from the originally selected task on first load. What am I missing?
In the designer:
<ListBox x:Name="listBoxTasks" ItemsSource="{Binding}" DisplayMemberPath="Key"
Grid.Row="0" Grid.Column="0" Grid.RowSpan="2">
</ListBox>
<ListBox x:Name="listBoxLogs"
ItemsSource="{Binding Logs}" DisplayMemberPath="EntryDate"
Grid.Row="1" Grid.Column="1">
</ListBox>
In the code behind:
public MainWindow()
{
InitializeComponent();
IMongoCollection<Task> tasks = DataManager.GetData();
this.DataContext = tasks.AsQueryable();
}
The Task class:
public class Task : BusinessBase<Task>
{
public ObjectId _Id { get; set; }
public string Key { get; set; }
public string Description { get; set; }
public string Summary { get; set; }
public string Details { get; set; }
public IEnumerable<Log> Logs { get; set; }
public IEnumerable<Link> Links { get; set; }
public IEnumerable<String> RelatedKeys { get; set; }
public IEnumerable<TaskItem> Items { get; set; }
}

Your Task class need to implement INotifyPropertyChanged interface so that as soon as there is any change in the underlying data it can tell WPF UI that something has changed now update/refresh your controls agains

Your task class need to implement INotifyPropertyChanged
http://msdn.microsoft.com/en-us/library/ms743695.aspx

You have to bind your first ListBox SelectedItem to object of Task model and add event handler for SelectionChanged. inside the this event you have to populate your logs by selected Task model also you have to implement INotifyPropertyChanged in your class.

It looks to me like the second binding should not work at all, as the DataContext is an enumerable of Tasks and the enumerable itself has no property called Logs. You could try working with IsSynchronizedWithCurrentItem and a binding to the current item:
<ListBox x:Name="listBoxTasks" ItemsSource="{Binding}" DisplayMemberPath="Key"
Grid.Row="0" Grid.Column="0" Grid.RowSpan="2"
IsSynchronizedWithCurrentItem="True"> <!-- Set this -->
</ListBox>
<ListBox x:Name="listBoxLogs" DisplayMemberPath="EntryDate"
Grid.Row="1" Grid.Column="1"
ItemsSource="{Binding /Logs}"> <!-- Note the slash which indicates a binding to the current item -->
</ListBox>
You could also bind to the SelectedItem of the other ListBox but this introduces a redundant dependency between the controls. Also note that if you change any property in your data-objects you need to implement the interface mentioned by the other answerers, INotifyPropertyChanged.

I have it all working now. I implemented INotifyPropertyChanged, although that didn't solve the problem.
I am now using the MVVM pattern. This helped...the NoRM library I was using didn't have a SelectionChanged event. I created a View Model and was able to convert those Models to ObservableCollections. Now I'm just setting the Logs control DataContext on selection changed for the Task class.

Related

Combobox binding issue

My ComboBox does not get populated with data.
Class Employee set to public, has variables such as:
public int EmployeeID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
Code on UserControl:
public IEnumerable<csEmployee> employeeList;
public ObservableCollection<csEmployee> _employeeSorted { get; set; }
public ucAddClient()
{
InitializeComponent();
//Establish connection
var GetMyData = new DataAccess();
//Get data by procedure
employeeList = GetMyDataPV.ExecuteStoredProc<csEmployee>("procedure", new {KeyDate = Key_to_extract});
employeeList = employeeList.Where(record => record.EmployeeLevelID > 300);
_employeeSorted = new ObservableCollection<csEmployee>(employeeList.Where(record => record != null));
}
And WPF:
<ComboBox x:Name="cbAddManager"
Foreground="#FF4D648B"
FontSize="12"
IsEditable="True"
ItemsSource="{Binding _employeeSorted}"
DisplayMemberPath="FirstName"
PreviewKeyDown="cbAddManager_PreviewKeyDown"
Width="200">
<!--<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Width ="50" Text="{Binding LastName}"/>
<TextBlock Text=", "/>
<TextBlock Width ="50" Text="{Binding FirstName}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>-->
</ComboBox>
Do you have any idea, why ComboBoxis not populated? When I do this in code (I add it in user control class) it gets data needed.
Im not sure if Im binding it correctly?
That is because you assign a new instance of a collection to your _employeeSorted property after InitializeComponent. At that time, the binding is already set up and does not get notified that you have updated the property from null, because you do not implement INotifyPropertyChanged.
There are multiple ways to solve the issue:
Initialize the collection before InitializeComponent and work on this same collection if you intend to change it, using Clear and Add instead of creating a new instance on changes.
Implement the INotifyPropertyChanged interface and use it to notify changes to your property so that the bindings are updated the the changes are applied in the user interface, e.g.:
public partial class MyUserControl : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<csEmployee> _employeeSortedField;
public ObservableCollection<csEmployee> _employeeSorted
{
get => _employeeSortedField;
set
{
if (_employeeSortedField == value)
return;
_employeeSortedField = value;
OnPropertyChanged();
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Expose a depenedency property for the collection instead and bind it to a collection in your view model that is passed as data context of the UserControl, thus moving the data access out it and separating the view from the business logic and data (recommended, see below MVVM).
Another issue might be that you do not set your data context to the UserControl itself in XAML (which is not recommened by the way, although it might solve your issue). In this case, the binding is unable to resolve the property at runtime (a binding error will be shown in the output window).
<UserControl x:Class="YourProject.YourControl"
...
DataContext="{Binding RelativeSource={RelativeSource Self}}">
As a note, it seems that you mix your business logic with your UserControl (view). Leverage the MVVM design pattern to create view models and seprate both concerns instead. Furthermore, if you set the data context of your UserControl to itself, you break data context inheritance.

Binding GridView to List<Object>

I've been scouring through the web (including stack overflow) for the solution to this problem. The current problem I'm having is that I'm binding that my data incorrectly to my View. When a user first navigates to the home page, the GridView shows nothing. When i go to another page and then come back to the home page, the list is shown except it is populated with strings that say "MyProject.Model.MyTask"
I know the issue isn't with my model or viewmodel as I'm able to retrieve the list properly with the data inside. One issue could be with my await call in my viewmodel. Any thoughts as to what I'm doing wrong in binding my List to the GridView?
ViewModel
public HomeViewModel(IDataService dataService, INavigationService navigationService) _dataService = dataService;
_navigationService = navigationService;
Initialize();
}
private async Task Initialize() {
MyTasks = await ApiHandlker.GetMyTasks();
}
private List<MyTask> _myTasks;
public List<MyTask> MyTasks {
get { return _myTasks; }
set { _myTasks = value; }
}
Model
public class MyTask {
public string TaskID { get; set; }
public string TaskName { get; set; }
public string TaskAssigneeID { get; set; }
//...
}
View
<GridView x:Name="gridViewMyTasks" ItemsSource="{Binding MyTasks}">
<TextBlock Text="{Binding TaskName}"></TextBlock>
</GridView>
Don't bind it to a list, bind instead to an ObservableCollection<MyTask>. You're populating the list AFTER the GridView has bound to it, so the GridView doesn't know it changed. If you're not populating it and instead simply setting it, you'll need to implement INotifyPropertyChanged.
Secondly, what you're essentially doing is setting the source to your list, then trying to add a TextBlock also as a source (and being ignored). What you're looking for instead is to set the ItemTemplate. Something like:
<GridView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TaskName}"/>
</DataTemplate>
</GridView.ItemTemplate>
See the documentation for more information.
You need to implement INotifyPropertyChanged and raise PropertyChanged in the setter of MyTasks.
You may find my recent MSDN article on asynchronous data binding helpful.
You're missing parts of the XAML:
<GridView x:Name="gridViewMyTasks" ItemsSource="{Binding MyTasks}">
<GridView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TaskName}"/>
<DataTemplate>
<GridView.ItemTemplate>
</GridView>

WPF command bindings to a DataItemTemplate with MVVM

I'm working on a WPF project, and I'm trying to follow the MVVM pattern. In the past I've only used DataTemplates to display information, but now I want to put a button on my template that performs an action related to the item containing the clicked button. I'm trying to figure out the best way to bind my code to my button in a way that the code knows which button was clicked.
My WindowViewModel contains relay commands which are exposed through command properties, as well as an ObservableCollection of `Items'.
public class WindowViewModel
{
public ICommand ChangeItemCommand { get; private set; }
public ObservableCollection<Item> Items {get;private set;}
public WindowViewModel()
{
ChangeItemCommand = new RelayCommand(new Action<object>(this.ChangeItem));
Items = new ObservableCollection<Item>();
}
public void ChangeItem(object o)
{
string key = (string)o;
//do something to the item with provided key
}
}
My ItemViewModel contains an ItemKey property to identify the item.
public class ItemViewModel
{
public string ItemName { get; private set; }
public string ItemKey { get; private set; }
}
My list box DataTemplate looks something like this.
<DataTemplate DataType="local:ItemViewModel">
<StackPanel Orientation="Horizontal">
<Label Content="{Binding ItemName}"/>
<Button Command="???" CommandParameter="{Binding ItemKey}"/>
</StackPanel>
</DataTemplate>
So I'm trying to figure out the best way to bind the button command to WindowViewModel.ChangeItemCommand of the WindowViewModel.
One option I'm considering would be to add a command property to the ItemViewModel which is set when instances are created by WindowViewModel
public class ItemViewModel
{
public string ItemName { get; private set; }
public string ItemKey { get; private set; }
public ICommand ChangeItemCommand{ get; private set; }
}
<Button Command="{Binding ChangeItemCommand}" CommandParameter="{Binding ItemKey}"/>
Alternatively I could bind directly to the WindowViewModel.ChangeItemCommand property by using a RelativeSource.
<Button
Command="{Binding Path=ChangeItemCommand,
RelativeSource={RelativeSource AncestorType={x:Type MyAppAWindow}}}"
CommandParameter="{Binding ItemKey}"/>
Note: I'm not entirely sure I did that right
So which would be recommended, or is there another better way?
Both options are fine but the real decision to make is that who should be the owner of this action, In MVVM view models should be designed first and then views should use them properly.
For e.g.
if this action is say RemoveItem then I would say it belongs to WindowViewModel (as this is about changing the collection which WindowViewModel exposes).
but say this action is RefreshItemData or ChangeItemDetails(e.g.ItemName or ItemStatus) then it belongs to ItemViewModel (as this ItemViewModel can be used in other windows supporting this action)
So, I would advice you to first design the ViewModels considering functionality, re-usability etc. and then use suitable binding feature (i.e. RelativeSource, ElementName, direct etc.).
The latter is the preferred one.
There are several ways and it boils down to preferences. There are a lot of MVVM Frameworks out there like MVVM-Light and Caliburn Micro that makes binding to commands way way easier compared to pure WPF CommandBindings.

Having trouble binding ViewModel to ComboBox

I have a viewmodel setup as the following
public class cDriveListVM
{
public string Drive { get; set; }
public cDriveListVM(string name)
{
Drive = name;
}
}
I declare the observablecollection in the window and set its datacontext to this observable collection.
public ObservableCollection<cDriveListVM> DriveList { get; set; }
private void dl()
{
DriveList = new ObservableCollection<cDriveListVM>();
DriveList.Add(new cDriveListVM("drive 1"));
DriveList.Add(new cDriveListVM("drive 2"));
this.DataContext = DriveList;
}
Xml for combobox:
<ComboBox x:Name="Drive_ComboBox" ItemsSource="{Binding Path=Drive}" HorizontalAlignment="Center" IsReadOnly="True" Grid.Column="0" Grid.Row="0" Width="300" Margin="10" SelectionChanged="Drive_Changed" Height="22" VerticalAlignment="Top"/>
I am just learning how to use Viewmodel so I am unsure what I am doing wrong, any help would be appreciated. I updated the xml file it results in the following combbox.
There are a few problems with this code.
One, the binding is set up wrong. Since the property with the viewmodel collection is DriveList, the binding should be ItemsSource="{Binding Path=DriveList}".
Two, you are attempting to display a field from your viewmodel, which is not doable. WPF's binding engine only works with properties, so the viewmodel should have a property:
public string Drive { get; set; }
And finally, the DisplayMemberPath should match the property name from the viewmodel: DisplayMemberPath="Drive".
Update: I just noticed that the DataContext is the observable collection itself -- I probably missed it on the first read. In that case, you want to bind directly to the data context:
ItemsSource="{Binding}"
And set DisplayMemberPath to the property you want to display:
DisplayMemberPath="Drive"

WPF call method when property changes

In C#, how can a method be called when a property changes (both method and property belong to the same class)?
e.g.,
class BrowserViewModel
{
#region Properties
public List<TreeViewModel> Status { get; private set; }
public string Conditions { get; private set; }
#endregion // Properties
// i'd like to call this method when Status gets updated
void updateConditions
{
/* Conditions = something depending on the TreeViewItem select status */
}
}
Binding
<TreeView Grid.Row="1"
x:Name="StatusTree"
ItemContainerStyle="{StaticResource TreeViewItemStyle}"
ItemsSource="{Binding Path=Status, Mode=OneTime}"
ItemTemplate="{StaticResource CheckBoxItemTemplate}"
/>
Use-Case (if you are curious)
The property Status is bound to a TreeView control in the xaml. When it is updated, I'd like to call a method that updates the property Conditions. This property is bound to a TextBox in the xaml.
I'm new to Eventing in C#, so am a little lost.
Edit
class TreeViewModel implements INotifyPropertyChanged.
Conditions is updated by getting the IsChecked Value from the TreeView.
The size of the Status List never changes. When a TreeViewItem is selected/unselected the TreeViewModel changes.
TreeViewModel source (FooViewModel on this page)
Binding code above.
Didn't have to change Binding Mode for IsChecked.
<HierarchicalDataTemplate
x:Key="CheckBoxItemTemplate"
ItemsSource="{Binding Children, Mode=OneTime}"
>
<StackPanel Orientation="Horizontal">
<!-- These elements are bound to a TreeViewModel object. -->
<CheckBox
Focusable="False"
IsChecked="{Binding IsChecked}"
VerticalAlignment="Center"
/>
<ContentPresenter
Content="{Binding Name, Mode=OneTime}"
Margin="2,0"
/>
</StackPanel>
</HierarchicalDataTemplate>
I assume you want updateConditions to fire whenever an item is added/removed/changed in your list, not if the list reference itself changes.
Since you're implementing INotifyPropertyChanged within your TreeViewModel, I think you'll want to use ObservableCollection<T> instead of a plain List<T>. Check it here: http://msdn.microsoft.com/en-us/library/ms668604.aspx
Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed.
class BrowserViewModel
{
#region Properties
public ObservableCollection<TreeViewModel> Status { get; private set; }
public string Conditions { get; private set; }
#endregion // Properties
// i'd like to call this method when Status gets updated
void updateConditions
{
/* Conditions = something */
}
public BrowserViewModel()
{
Status = new ObservableCollection<TreeViewModel>();
Status.CollectionChanged += (e, v) => updateConditions();
}
}
CollectionChanged will fire whenever an item is added/removed/changed. As far as I know, it will consider it "changed" when its reference changes or any of its properties are changed (which is notified through INotifyPropertyChanged)
Just checked it here: http://msdn.microsoft.com/en-us/library/ms653375.aspx
ObservableCollection.CollectionChanged Event
Occurs when an item is added, removed, changed, moved, or the entire list is refreshed.
ObservableCollection<T> resides in the System.Collections.ObjectModel namespace, in System.dll assembly.

Categories