Autoscroll ListView in WPF using MVVM - c#

I try to have a list that automatically scrolls to the end when adding a new line.
Here is a simple MVVM example in which I want to integrate :
There is a button that adds lines to the list when clicked.
Model code :
public class Student
{
public string Lastname {get; set;}
public string Firstname {get; set;}
public Student(string lastname, string firstname) {
this.Lastname = lastname;
this.Firstname = firstname;
}
}
public class StudentsModel: ObservableCollection<Student>
{
private static object _threadLock = new Object();
private static StudentsModel current = null;
public static StudentsModel Current {
get {
lock (_threadLock)
if (current == null)
current = new StudentsModel();
return current;
}
}
private StudentsModel() {
for (int i = 1; i <= 50; i++)
{
Student aStudent = new Student("Student " + i.ToString(), "Student " + i.ToString());
Add(aStudent);
}
}
public void AddAStudent(String lastname, string firstname) {
Student aNewStudent = new Student(lastname, firstname);
Add(aNewStudent);
}
}
ViewModel code :
public class MainViewModel : ViewModelBase
{
public StudentsModel Students { get; set; }
public MainViewModel()
{
Students = StudentsModel.Current;
}
private ICommand _AddStudent;
public ICommand AddStudent
{
get
{
if (_AddStudent == null)
{
_AddStudent = new DelegateCommand(delegate()
{
Students.AddAStudent("New Student lastname", "New Student firstname");
});
}
return _AddStudent;
}
}
View code :
<Window x:Class="demo.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:demo.Commands">
<Grid>
<ListView Grid.Row="2" BorderBrush="White" ItemsSource="{Binding Path=Students}"
HorizontalAlignment="Stretch">
<ListView.View>
<GridView>
<GridViewColumn Header="Lastname" DisplayMemberBinding="{Binding Path=Lastname}" />
<GridViewColumn Header="Firstname" DisplayMemberBinding="{Binding Path=Firstname}" />
</GridView>
</ListView.View>
</ListView >
<Button Content="Add" Command="{Binding AddStudent}" Margin="601.94,36.866,96.567,419.403" />
</Grid>
Thank you

I wrote a simple AttachedProperty that I use to auto-scroll to the bottom when a bound ObservableCollection changes:
public class AutoScroller : Behavior<ScrollViewer>
{
public object AutoScrollTrigger
{
get { return (object)GetValue( AutoScrollTriggerProperty ); }
set { SetValue( AutoScrollTriggerProperty, value ); }
}
public static readonly DependencyProperty AutoScrollTriggerProperty =
DependencyProperty.Register(
"AutoScrollTrigger",
typeof( object ),
typeof( AutoScroller ),
new PropertyMetadata( null, ASTPropertyChanged ) );
private static void ASTPropertyChanged( DependencyObject d, DependencyPropertyChangedEventArgs args )
{
var ts = d as AutoScroller;
if( ts == null )
return;
// must be attached to a ScrollViewer
var sv = ts.AssociatedObject as ScrollViewer;
// check if we are attached to an ObservableCollection, in which case we
// will subscribe to CollectionChanged so that we scroll when stuff is added/removed
var ncol = args.NewValue as INotifyCollectionChanged;
// new event handler
if( ncol != null )
ncol.CollectionChanged += ts.OnCollectionChanged;
// remove old eventhandler
var ocol = args.OldValue as INotifyCollectionChanged;
if( ocol != null )
ocol.CollectionChanged -= ts.OnCollectionChanged;
// also scroll to bottom when the bound object itself changes
if( sv != null && ts.AutoScroll )
sv.ScrollToBottom();
}
private void OnCollectionChanged(object sender, EventArgs args)
{
App.Current.Dispatcher.Invoke(delegate {
(this.AssociatedObject as ScrollViewer).ScrollToBottom();
});
}
}
Note: I use Rx to subscribe to the CollectionChanged event, but that can be done with normal .NET event handling plus Dispatcher.Invoke to get the .ScrollToBottom() call on the UI thread.
Also Note: This attached property is in a Behavior class called TouchScroller, which does other stuff too, but it can be simplified to just a simple attached property.
EDIT:
To use it, in the xaml you would simply bind the property from the viewmodel:
<ScrollViewer ...>
<i:Interaction.Behaviors>
<util:TouchScroller AutoScrollTrigger="{Binding Students}" />
</i:Interaction.Behaviors>
...
</ScrollViewer>
EDIT2:
I edited the code to contain a full Behavior. I use a Behavior instead of simpley a static class with an attached behavior because it gives access to .AssociatedObject, which is the ScrollViewer that you need to call .ScrollToBottom() on. Without using a behavior, you would have to keep track of these objects manually. I also removed the Rx subscription and added simple event handlers and Dispatcher.Invoke.
This is a stripped down and modified version of what I use, but I haven't tested this version.

Related

How to get View to update when the ViewModel gets changed

I'm building a WPF application to manage a student database stored in a SharePoint list. I'm new to using MVVM and have gone through a couple tutorials. I have gotten as far as having successfully created a view and model and have managed to bind it to a datagrid control. What I would like to do is to update the data in the view based on the output of a combobox.
Here's my model:
using System.ComponentModel;
namespace StudentManagement.Model
{
public class Student : INotifyPropertyChanged
{
private string _Title;
private string _FullName;
public string Title
{
get { return _Title; }
set
{
if (_Title != value)
{
_Title = value;
RaisePropertyChanged("Title");
}
}
}
public string FullName
{
get { return _FullName; }
set
{
if (_FullName != value)
{
_FullName = value;
RaisePropertyChanged("FullName");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
}
Here's the viewmodel:
using System.Collections.ObjectModel;
using StudentManagement.Model;
using SP = Microsoft.SharePoint.Client;
using StudentManagement.publicClasses;
namespace StudentManagement.ViewModel
{
public class StudentViewModel
{
public ObservableCollection<Student> Students { get; set; }
public void LoadStudents(string query)
{
ObservableCollection<Student> _students = new
ObservableCollection<Student>();
SP.ClientContext ctx = clientContext._clientContext;
SP.CamlQuery qry = new SP.CamlQuery();
qry.ViewXml = query;
SP.ListItemCollection splStudents =
ctx.Web.Lists.GetByTitle("Students").GetItems(qry);
ctx.Load(splStudents);
ctx.ExecuteQuery();
foreach (SP.ListItem s in splStudents)
{
_students.Add(new Student { Title = (string)s["Title"], FullName = (string)s["FullName"] });
}
Students = _students;
}
}
}
Here's my XAML
<UserControl x:Class="StudentManagement.Views.StudentsView"
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"
mc:Ignorable="d"
xmlns:local="clr-namespace:StudentManagement.Views" >
<Grid>
<StackPanel HorizontalAlignment="Left" Width="200" >
<TextBox Name="txtSearch" AcceptsReturn="True" ></TextBox>
<ComboBox Name="cmbStatus" SelectionChanged="cmbStatus_SelectionChanged" SelectedIndex="0">
<ComboBoxItem>Active</ComboBoxItem>
<ComboBoxItem>Inquiring</ComboBoxItem>
<ComboBoxItem>Inactive</ComboBoxItem>
<ComboBoxItem>Monitoring</ComboBoxItem>
</ComboBox>
<DataGrid Name="dgStudentList" ItemsSource="{Binding Path=Students}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Title, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="100"/>
<DataGridTextColumn Header="Parent" Binding="{Binding FullName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="100" />
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Grid>
</UserControl>
...and the code behind for the view:
using System.Windows.Controls;
using StudentManagement.ViewModel;
namespace StudentManagement.Views
{
/// <summary>
/// Interaction logic for StudentsView.xaml
/// </summary>
public partial class StudentsView : UserControl
{
private StudentViewModel _viewModel = new
StudentViewModel();
public StudentsView()
{
InitializeComponent();
DataContext = _viewModel;
}
private void cmbStatus_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
string combotext = ((sender as ComboBox).SelectedItem as ComboBoxItem).Content as string;
string qry = #"<View>
<Query>
<Where><Eq><FieldRef Name='Current_x0020_Status' /><Value Type='Choice'>" + combotext + #"</Value></Eq></Where>
</Query>
</View>";
_viewModel.LoadStudents(qry);
}
}
}
As it stands now, the students get loaded into the datagrid on load just fine. When the event cmbStatus_SelectionChanged fires i've done tests and I can see that the LoadStudents function fires and returns the correct number of entries, but nothing gets updated on the datagrid.
I'm sure this a noob mistake and I'm missing something basic but this one is doing my head in and I'd appreciate any guidance.
Since StudentViewModel.LoadStudents() changes the value of the Students property, the view model needs to notify the view that this changed. You can do this by having StudentViewModel implement INotifyPropertyChanged (just like Student does). The DataGrid will subscribe to the PropertyChanged event, and will update its contents when that event is fired.
You are initializing your Students collection every time if the ComboBox's selection changed.
ObservableCollection<Student> _students = new ObservableCollection<Student>();
You should not do this with a bound collection in ViewModel. You can clear the collection and add new items like this.
public class StudentViewModel
{
public ObservableCollection<Student> Students { get; set; } = new ObservableCollection<Student>();
public void LoadStudents(string query)
{
Students.Clear();
SP.ClientContext ctx = clientContext._clientContext;
SP.CamlQuery qry = new SP.CamlQuery();
qry.ViewXml = query;
SP.ListItemCollection splStudents = ctx.Web.Lists.GetByTitle("Students").GetItems(qry);
ctx.Load(splStudents);
ctx.ExecuteQuery();
foreach (SP.ListItem s in splStudents)
{
Students.Add(new Student { Title = (string)s["Title"], FullName = (string)s["FullName"] });
}
}
}

MVVM C# WPF - UI not updating when changing items in observablecollection

I've been looking at Stackoverflow posts regarding this issue for two days now and I cant seem to understand why my code isn't working
I can't seem to get the datagrid in my UI to update when I change an item within a ObservableCollection.
I know that the ObservableCollection does not trigger a PropertyChanged event if a item within it changes.
It seems like others have successfully done this by adding INotifyPropertyChanged to the Model, and calling OnPropertyChanged when the property is changed. I've implemented this and I've checked that the PropertyChanged event is going off.
The UI updates when adding new items to the collection.
I realize this might be a threading issue, but I don't really understand how to check this or fix it.
I'm fairly new to programming and trying to wrap my head around MVVM.
Any suggestions?
Model:
public class ModelObj : INotifyPropertyChanged
{
public string Name { get; set; }
public string IpAddress { get; set; }
private DateTime timer;
public DateTime Timer
{
get { return timer; }
set
{
timer = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
ViewModel:
public class MainViewModel : ViewModelBase
{
public ObservableCollection<ModelObj> ModelObjects { get; } = new ObservableCollection<ModelObj>();
private IUdpDataService _udpDataService;
public MainViewModel(IUdpDataService udpDataService)
{
_udpDataService = udpDataService;
}
public void StartUdpDataService()
{
_udpDataService.StartBroadCasting();
_udpDataService.ReceivedDataEvent += ParseReceivedData;
}
private void ParseReceivedData(string receivedData)
{
// This object contains all the information in the received data packet.
UdpPacket udpPacket = new UdpPacket(receivedData);
// This object only contains the object name, IpAddress and a time variable.
ModelObj modelObj = new ModelObj
{
Name = udpPacket.Name,
IpAddress = udpPacket.IpEthernet,
Timer = DateTime.Now,
};
App.Current.Dispatcher.Invoke((Action)delegate
{
UpdateList(modelObj);
});
}
private void UpdateList(ModelObj modelObj)
{
var testObj = ModelObjects.FirstOrDefault(x => x.Name == modelObj.Name);
if (testObj != null)
{
testObj = modelObj
}
else
{
ModelObjects.Add(modelObj);
testObj = modelObj;
}
}
}
View:
public partial class MainWindow : Window
{
private MainViewModel _viewModel;
public MainWindow(MainViewModel viewModel)
{
InitializeComponent();
_viewModel = viewModel;
DataContext = _viewModel;
Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
_viewModel.StartUdpDataService();
}
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
Settings.Default.Save();
base.OnClosing(e);
}
}
XAML:
<DataGrid Grid.Row="0" ItemsSource="{Binding Path=ModelObjects}"
IsReadOnly="True"
Background="white"
RowHeaderWidth ="0"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="auto" MinWidth="150"/>
<DataGridTextColumn Header="IP address" Binding="{Binding IpAddress}" Width="*"/>
<DataGridTextColumn Header="Timer" Binding="{Binding Timer, UpdateSourceTrigger=PropertyChanged}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
You should set the Timer property of the existing object:
private void UpdateList(ModelObj modelObj)
{
var testObj = ModelObjects.FirstOrDefault(x => x.Name == modelObj.Name);
if (testObj != null)
{
testObj.Timer = modelObj.Timer
}
else
{
ModelObjects.Add(modelObj);
}
}
You are currently getting a reference to the existing object and then set the testObj variable that holds this reference to the reference to the new ModelObj object that is passed to the UpdateList method. This won't update the Timer property of the object that's in the ModelObjects collection.
testObj = modelObj
Does not have effect. You just putting a value in to variable. Call
if (testObj != null)
{
ModelObjects.Replace(testObj,modelObj)
}
else
{
ModelObjects.Add(modelObj);
}
and if you replacing entire object there is no need to implement INotifyPropertyChanged at all

ComboBox, is it possible to have ItemsSource and SelectedValue bound to different sources?

I have two different objects that are pointing at each other. The first object represents a division in a company. That object has two collection: Employees, which is all the employees working in the division and Project, which is all the special projects that are in progress within that division. So the first object looks like this:
public class Division : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
ObservableCollection<Employee> _employees;
ObservableCollection<Project> _projects;
public Division()
{
Employees = new ObservableCollection<Employee>();
Projects = new ObservableCollection<Project>();
}
public ObservableCollection<Employee> Employees
{
get { return _employees; }
set
{
if (_employees != value)
{
_employees = value;
PropertyChanged(this, new PropertyChangedEventArgs("Employees"));
}
}
}
public ObservableCollection<Project> Projects
{
get { return _projects; }
set
{
if (_projects != value)
{
_projects = value;
PropertyChanged(this, new PropertyChangedEventArgs("Projects"));
}
}
}
public void AddNewProject()
{
this.Projects.Add(new Project(this));
}
}
Notice that when adding a new project to the division, I pass a reference to the division into that project, which looks like this:
public class Project : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
string _projectName;
DateTime _deadline = DateTime.Now;
Division _division;
ObservableCollection<Employee> _members;
public Project()
{
Members = new ObservableCollection<Employee>();
}
public Project(Division div)
{
Members = new ObservableCollection<Employee>();
Division = div;
}
public string ProjectName
{
get { return _projectName; }
set
{
if (_projectName != value)
{
_projectName = value;
PropertyChanged(this, new PropertyChangedEventArgs("ProjectName"));
}
}
}
public DateTime Deadline
{
get { return _deadline; }
set
{
if (_deadline != value)
{
_deadline = value;
PropertyChanged(this, new PropertyChangedEventArgs("Deadline"));
}
}
}
public Division Division
{
get { return _division; }
set
{
if (_division != value)
{
if (_division != null)
{
_division.Employees.CollectionChanged -= members_CollectionChanged;
}
_division = value;
if (_division != null)
{
_division.Employees.CollectionChanged += members_CollectionChanged;
}
PropertyChanged(this, new PropertyChangedEventArgs("Division"));
}
}
}
public ObservableCollection<Employee> Members
{
get { return _members; }
set
{
if (_members != value)
{
if (_members != null)
{
_members.CollectionChanged -= members_CollectionChanged;
}
_members = value;
if (_members != null)
{
_members.CollectionChanged += members_CollectionChanged;
}
PropertyChanged(this, new PropertyChangedEventArgs("Members"));
}
}
}
public ObservableCollection<Employee> AvailableEmployees
{
get
{
if (Division != null){
IEnumerable<Employee> availables =
from s in Division.Employees
where !Members.Contains(s)
select s;
return new ObservableCollection<Employee>(availables);
}
return new ObservableCollection<Employee>();
}
}
void members_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
PropertyChanged(this, new PropertyChangedEventArgs("AvailableEmployees"));
}
}
The reason I'm doing it like this is, that the project could have any type of team working on it, but only from within the division. So, when building a dashboard for the division, the manager could select any of the employees to that project but without putting in an employee that is already assigned to it. So, the AvailableEmployees property in the project object always keeps track of who is not already assigned to that project.
The problem I'm having is how to translate this into a UI. The experiment I've done so far looks like this:
<UserControl x:Class="Test.Views.TestView"
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"
mc:Ignorable="d"
xmlns:local="clr-namespace:Test.Views"
d:DesignHeight="300" d:DesignWidth="300">
<StackPanel>
<ListBox ItemsSource="{Binding Div.Projects}">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Background="Transparent"
BorderThickness="0, 0, 0, 2"
BorderBrush="Black"
Margin="0, 0, 0, 5"
Padding="0, 0, 0, 5">
<StackPanel>
<TextBox Text="{Binding ProjectName}"/>
<ListBox ItemsSource="{Binding Members}">
<ListBox.ItemTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=local:TestView}, Path=DataContext.AvailableEmployees}"
DisplayMemberPath="FirstName"
Text="{Binding FirstName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="Add Employee to Project"
Command="{Binding RelativeSource={RelativeSource AncestorType=local:TestView}, Path=DataContext.AddEmployeeToProject}"
CommandParameter="{Binding}"/>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="Add New Project"
Command="{Binding AddNewProject}" />
</StackPanel>
The view model associated with this view is as follows:
public class TestViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private Division _div;
public TestViewModel(Division div)
{
Div = div;
AddNewProject = new DelegateCommand(OnAddNewProject);
AddEmployeeToProject = new DelegateCommand<Project>(OnAddEmployeeToProject);
}
public DelegateCommand AddNewProject { get; set; }
public DelegateCommand<Project> AddEmployeeToProject { get; set; }
public Division Div
{
get { return _div; }
set
{
if (_div != value)
{
_div = value;
PropertyChanged(this, new PropertyChangedEventArgs("Div"));
}
}
}
private void OnAddNewProject()
{
Div.AddNewProject();
}
private void OnAddEmployeeToProject(Project proj)
{
var availables = proj.AvailableEmployees;
if (availables.Count > 0)
{
proj.Members.Add(availables[0]);
}
}
}
However, I cannot get the combobox for each employee in each project to work. It seems like the selected item/value is bound to the itemssource, and each time the combobox turns out blank. I've tried to do this also with SelectedValue and SelectedItem properties for the combobox, but none worked.
How do I get these two separated. Is there anything else I'm missing here?
OK. After so many experiments the best solution I came up with was to create my own user control that is composed of both a button and a combobox that imitate the behavior I was expecting of the combobox on it own.
First, I had a really stupid mistake in the model where both lists of members Project and Division contain the same instances of Employee, which makes the AvailableEmployees property buggy. What I really needed to do is to create a list of copies of employees in the Project instead of just references.
In any case, I created a new user control and called it DynamicSourceComboBox. The XAML of this control looks like this:
<Grid>
<Button x:Name="selected"
Content="{Binding RelativeSource={RelativeSource AncestorType=local:DynamicSourceComboBox}, Path=SelectedValue}"
Click="selected_Click"/>
<ComboBox x:Name="selections"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=local:DynamicSourceComboBox}, Path=ItemsSource}"
DisplayMemberPath="{Binding RelativeSource={RelativeSource AncestorType=local:DynamicSourceComboBox}, Path=DisplayMemberPath}"
Visibility="Collapsed"
SelectionChanged="selections_SelectionChanged"
MouseLeave="selections_MouseLeave"/>
</Grid>
I have here a few bindings from the button and the combobox to properties in my user control. These are actually dependency properties. The code-behind of my user control looks like this:
public partial class DynamicSourceComboBox : UserControl
{
public DynamicSourceComboBox()
{
InitializeComponent();
}
public object SelectedValue
{
get { return (object)GetValue(SelectedValueProperty); }
set { SetValue(SelectedValueProperty, value); }
}
public static readonly DependencyProperty SelectedValueProperty =
DependencyProperty.Register("SelectedValue", typeof(object), typeof(DynamicSourceComboBox), new PropertyMetadata(null));
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public static readonly DependencyProperty ItemsSourceProperty =
ComboBox.ItemsSourceProperty.AddOwner(typeof(DynamicSourceComboBox));
public string DisplayMemberPath
{
get { return (string)GetValue(DisplayMemberPathProperty); }
set { SetValue(DisplayMemberPathProperty, value); }
}
public static readonly DependencyProperty DisplayMemberPathProperty =
ComboBox.DisplayMemberPathProperty.AddOwner(typeof(DynamicSourceComboBox));
private void selected_Click(object sender, RoutedEventArgs e)
{
selected.Visibility = Visibility.Hidden;
selections.Visibility = Visibility.Visible;
selections.IsDropDownOpen = true;
}
private void selections_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
selections.Visibility = Visibility.Collapsed;
selected.Visibility = Visibility.Visible;
selections.IsDropDownOpen = false;
if (e.AddedItems.Count == 1)
{
var item = e.AddedItems[0];
Type itemType = item.GetType();
var itemTypeProps = itemType.GetProperties();
var realValue = (from prop in itemTypeProps
where prop.Name == DisplayMemberPath
select prop.GetValue(selections.SelectedValue)).First();
SelectedValue = realValue;
}
}
private void selections_MouseLeave(object sender, MouseEventArgs e)
{
selections.Visibility = Visibility.Collapsed;
selected.Visibility = Visibility.Visible;
selections.IsDropDownOpen = false;
}
}
These dependency properties imitate the properties with similar names in ComboBox but they are hooked up to the internal combobox and the button in a way that makes them behave together as a single complex combobox.
The Click event in the button hides it and present the combobox to make the effect of just a box that is opening. Then I have a SelectionChanged event in the combobox firing to update all the needed information and a MouseLeave event just in case the user doesn't make any real selection change.
When I need to use the new user control, I set it up like this:
<local:DynamicSourceComboBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorLevel=1, AncestorType=ListBox}, Path=DataContext.AvailableEmployees}"
DisplayMemberPath="FirstName"
SelectedValue="{Binding FirstName, Mode=TwoWay}"/>
Of course, for all of it to work, I have to make a lot of hookups with PropertyChanged events in the models, so the Projects instance will know to raise a PropertyChanged event for AvailableEmployees any time a change is made, but this is not really the concern of this user control itself.
This is a pretty clunky solution, with a lot of extra code that is a bit hard to follow, but it's really the best (actually only) solution I could have come up with to the problem I had.

INotifyPropertChanged Updating Bound Listview/gridview

Okay so the problem is that my INotifyPropertyChanged isnt updting the list view n XAML
DiscoveredData.NetworkedComputersResults = NetworkedComputers; < this is where it loads the data into the DataContext and then calls the iproperty notify changed.
ListView_LocalComputers.ItemsSource = DiscoveredData.NetworkedComputersResults; < using this works fine and i can see all my data however this apparantly not the way to do it.
since i know that i can load the data into the list view using the ItemsSource im thinking the problem is in the XAML.
i would be greatful if someone could point me in the right direction.
Also if you see that i am doing this incorrectly please advise, im fairly new at this coding language and would like to do it the right way
Thank you in advance
<ListView Name="ListView_LocalComputers" ItemsSource="{Binding NetworkedComputerResults}">
<ListView.View>
<GridView>
<GridViewColumn Header="Status">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Border CornerRadius="2,2,2,2" Width="20" Height="20" Background="Transparent" BorderBrush="Transparent" Margin="3,3,3,3">
<Image HorizontalAlignment="Left" VerticalAlignment="Center" Width="12" Height="12" Source="{Binding Image}" Stretch="Fill" Margin="2,2,2,2"/>
</Border>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Local Computers" DisplayMemberBinding="{Binding ComputerName}">
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
//Constructor
public NetworkInformation()
{
InitializeComponent();
this.DataContext = DiscoveredData; //Defines the class to the view
Discovery();
}
//Method
public void Discovery()
{
GetIcon Icon = new GetIcon();
BitmapImage IconOfComputer = null;
List<DiscoveredComputer> NetworkedComputers = new List<DiscoveredComputer>();
DirectoryEntry Discover = new DirectoryEntry("WinNT://Workgroup");
BitmapImage On = Icon.LoadIcon(#"/Images/Icons/ComputerOn.ico");
BitmapImage Off = Icon.LoadIcon(#"/Images/Icons/ComputerOff.ico");
foreach (DirectoryEntry Node in Discover.Children)
{
try
{
if (Node.Properties.Count > 0)
{
IconOfComputer = On;
}
}
catch
{
IconOfComputer = Off;
}
if (Node.Name != "Schema") { NetworkedComputers.Add(new DiscoveredComputer { Image = IconOfComputer, ComputerName = Node.Name, MyToolTip = "Node Type = " + Node.SchemaEntry.Name }); }
}
DiscoveredData.NetworkedComputersResults = NetworkedComputers;
ListView_LocalComputers.ItemsSource = DiscoveredData.NetworkedComputersResults;
}
private class GetIcon
{
public BitmapImage IconStorage { get; set; }
public BitmapImage LoadIcon(String IconPath)
{
BitmapImage GeneratedIcon = new BitmapImage();
GeneratedIcon.BeginInit();
GeneratedIcon.UriSource = new Uri("pack://application:,,," + IconPath, UriKind.RelativeOrAbsolute);
GeneratedIcon.EndInit();
IconStorage = GeneratedIcon;
return GeneratedIcon;
}
}
public class NetworkData : INotifyPropertyChanged
{
#region Property Notify Standard for all classes
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
#endregion
#region Bound Data To View
private List<DiscoveredComputer> _NetworkedComputersResults;
public List<DiscoveredComputer> NetworkedComputersResults {
get { return _NetworkedComputersResults; }
set
{
_NetworkedComputersResults = value;
NotifyPropertyChanged("NetworkedComputersResults");
}
}
#endregion
public class DiscoveredComputer : NetworkData
{
public string ComputerName { get; set; }
public BitmapImage Image { get; set; }
public String MyToolTip { get; set; }
}
}
You should use ObservableCollection. It implements INotifyCollectionChanged which notifies when a collection changed, not just a single Item.
The items themselves should implement INotifyPropertyChanged of course...
You are binding a plain List<T> to your ListView. However this works fine, it will not fulfill the requirement of dynamic updating the list view when items are added / removed.
If you need the dynamic add/remove elements in your ListView simply use an ObservableCollection<T> instead of the List`.
private ObservableCollection<DiscoveredComputer> _NetworkedComputersResults;
public ObservableCollection<DiscoveredComputer> NetworkedComputersResults {
get { return _NetworkedComputersResults; }
set
{
_NetworkedComputersResults = value;
NotifyPropertyChanged("NetworkedComputersResults");
}
}
If all you need is elements be dynamically be added/removed, then the elements in the observable collection do not need to implement the INotifyPropertyChanged interface.
public class NetworkData
{
public NetworkData()
{
NetworkedComputersResults = new ObservableCollection<DiscoveredComputer>();
}
public ObservableCollection<DiscoveredComputer> NetworkedComputersResults{get;set;}
}
DiscoveryMethod
public void Discovery()
{
GetIcon Icon = new GetIcon();
BitmapImage IconOfComputer = null;
List<DiscoveredComputer> NetworkedComputers = new List<DiscoveredComputer>();
DirectoryEntry Discover = new DirectoryEntry("WinNT://Workgroup");
BitmapImage On = Icon.LoadIcon(#"/Images/Icons/ComputerOn.ico");
BitmapImage Off = Icon.LoadIcon(#"/Images/Icons/ComputerOff.ico");
foreach (DirectoryEntry Node in Discover.Children)
{
try
{
if (Node.Properties.Count > 0)
{
IconOfComputer = On;
}
}
catch
{
IconOfComputer = Off;
}
if (Node.Name != "Schema") { NetworkedComputers.Add(new DiscoveredComputer { Image = IconOfComputer, ComputerName = Node.Name, MyToolTip = "Node Type = " + Node.SchemaEntry.Name }); }
}
//Use Clear and Add .Dont assign new instance DiscoveredData.NetworkedComputersResults=new ....
DiscoveredData.NetworkedComputersResults.Clear();
foreach (var item in NetworkedComputers)
{
DiscoveredData.NetworkedComputersResults.Add(item);
}
}
I hope this will help.From my personal View it would be good If you create this Discovery method in ViewModel and would Call it from the Constructor of ViewModel . It seems two way communication like you setting property of ViewModel from View thats the job of Binding not code behind

DataTemplate to show Buttons in WPF ListView while maintaing Properties

In WPF, I have a ListView of 2 columns and the first column needs to be a button. Correct me if I'm wrong, but the only way I found to implement a button in a ListView is to use a DataTemplate. The problem I found with this is I have no way to maintain my original button Properties when they are mapped with a DataTemplate so I am forced to use binding to remap every individual property (including custom Properties since I'm actually using a custom User Control which inherits from Button). This seems extraneous to have to manually map all Properties so maybe there's a better way to automatically persist those properties?
Here's my test code:
public MainWindow() {
InitializeComponent();
ObservableCollection<ScreenRequest> screenRequests = new ObservableCollection<ScreenRequest>() {
new ScreenRequest("A", "1"),
new ScreenRequest("B", "2")
};
myListView.ItemsSource = screenRequests;
}
public class ScreenRequest {
public CustomButton ScreenButton { set; get; }
public string Details { set; get; }
public ScreenRequest(string buttonText, string customProperty) {
this.ScreenButton = new CustomButton();
this.ScreenButton.Content = buttonText;
this.ScreenButton.CustomProperty = customProperty;
this.ScreenButton.Click += new RoutedEventHandler(InitiateScreenRequest);
}
private void InitiateScreenRequest(object sender, RoutedEventArgs e) {
CustomButton screenBtn = (CustomButton)sender;
screenBtn.Content = "BUTTON TEXT CHANGED";
}
}
public class CustomButton : Button {
public string CustomProperty { get; set; }
}
And the XAML:
<Window...
...
<Window.Resources>
<DataTemplate x:Key="ButtonTemplate">
<local:CustomButton Content="{Binding ScreenButton.Content}"/>
</DataTemplate>
</Window.Resources>
<Grid x:Name="grdMain">
...
<ListView...
<ListView.View>
<GridView x:Name="gridView">
<GridViewColumn CellTemplate="{StaticResource ButtonTemplate}" Width="Auto" Header="Screen" HeaderStringFormat="Screen"/>
<GridViewColumn Header="Details" HeaderStringFormat="Details" DisplayMemberBinding="{Binding Details}"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
So my questions are:
Do I have to manually map every single property in the CustomButton in order for it to carry over to the DataTemplate or is their a catch-all to automatically persist the Properties?
How do I map the CustomProperty Property in the binding such that it sticks with the button? Do I use a DependencyProperty for this?
How do I maintain my click event such that clicking the button in GridView will call the InitiateScreenRequest function? Ideally I'd like to have a single method declared for all buttons, but I haven't gotten to that point yet.
Any help or insight into buttons in listviews would be appreciated.
<Window x:Class="MiscSamples.TonyRush"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="TonyRush" Height="300" Width="300">
<ListView ItemsSource="{Binding}">
<ListView.View>
<GridView>
<GridViewColumn Width="Auto" Header="Screen" HeaderStringFormat="Screen">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button Command="{Binding SomeAction}" Content="{Binding ActionDescription}" Width="100"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Details" HeaderStringFormat="Details" DisplayMemberBinding="{Binding Details}" Width="100"/>
</GridView>
</ListView.View>
</ListView>
</Window>
Code Behind:
public partial class TonyRush : Window
{
public TonyRush()
{
InitializeComponent();
DataContext = new List<ScreenRequest>
{
new ScreenRequest() {ActionDescription = "Click Me!"},
new ScreenRequest() {ActionDescription = "Click Me Too!"},
new ScreenRequest() {ActionDescription = "Click Me Again!!"},
};
}
}
ViewModel:
public class ScreenRequest: INotifyPropertyChanged
{
public Command SomeAction { get; set; }
private string _actionDescription;
public string ActionDescription
{
get { return _actionDescription; }
set
{
_actionDescription = value;
NotifyPropertyChanged("ActionDescription");
}
}
private string _details;
public string Details
{
get { return _details; }
set
{
_details = value;
NotifyPropertyChanged("Details");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public ScreenRequest()
{
SomeAction = new Command(ExecuteSomeAction) {IsEnabled = true};
}
//public SomeProperty YourProperty { get; set; }
private void ExecuteSomeAction()
{
//Place your custom logic here based on YourProperty
ActionDescription = "Clicked!!";
Details = "Some Details";
}
}
Key part: The Command class:
//Dead-simple implementation of ICommand
//Serves as an abstraction of Actions performed by the user via interaction with the UI (for instance, Button Click)
public class Command : ICommand
{
public Action Action { get; set; }
public void Execute(object parameter)
{
if (Action != null)
Action();
}
public bool CanExecute(object parameter)
{
return IsEnabled;
}
private bool _isEnabled;
public bool IsEnabled
{
get { return _isEnabled; }
set
{
_isEnabled = value;
if (CanExecuteChanged != null)
CanExecuteChanged(this, EventArgs.Empty);
}
}
public event EventHandler CanExecuteChanged;
public Command(Action action)
{
Action = action;
}
}
Result:
Notes:
Take a look at how separate UI is from Data and functionality. This is the WPF way. Never mix UI with data / business code.
The Command in the ViewModel serves as an abstraction for the Button. The ViewModel doesn't know what a Button is, nor should it. Let me know if you need further details.

Categories