I have a program that searches a directory for files matching certain criteria. This search process takes a long time, so I have to call it asynchronously. When the search algorithm finds a file, it triggers an event. My MainWindow instance listens for this event and needs to update the GUI. How can I bind these "added" files to a ListView? I figured that I could use an ObservableCollection<FileInfo> instance, but I can't figure out how to bind it.
I've stripped out all of the irrelevant controls and code. Here are the two relevant files.
MainWindow.xaml:
<Window x:Class="Example.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="CR Search" Height="395" Width="525">
<Grid>
<ListView x:Name="Results">
<ListView.View>
<GridView>
<GridViewColumn Header="Filename"/>
<GridViewColumn Header="Directory"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
MainWindow.xaml.cs:
using System.IO;
using System.Threading.Tasks;
public partial class MainWindow
{
private SearchLogic _backgroundSearch;
private async void Search(object sender, RoutedEventArgs e)
{
// TODO: clear Results
_backgroundSearch = new SearchLogic("", new DirectoryInfo("C:\"));
_backgroundSearch.FileAdded += FileAdded;
await Task.Run(new Action(_backgroundSearch.Search));
}
private void FileAdded(object sender, FileAddedEventArgs eventArgs)
{
// TODO: add eventArgs.File to Results
// eventArgs.File is an instance of FileInfo
}
}
Here is a simple example
Your XAML
<Window x:Class="WpfApplication10.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Width="525"
Height="350"
Loaded="Window_Loaded">
<Grid>
<ListBox ItemsSource="{Binding FileNames}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<Label>Name</Label>
<TextBlock Text="{Binding Name}"/>
<Label>Modified</Label>
<TextBlock Text="{Binding LastModified}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
Your Code Behind
public partial class MainWindow : Window
{
public class FileInfo
{
public string Name { get; set; }
public DateTime LastModified { get; set; }
public FileInfo(string name)
{
Name = name;
LastModified = DateTime.Now;
}
}
ObservableCollection<FileInfo> mFileNames = new ObservableCollection<FileInfo>();
public ObservableCollection<FileInfo> FileNames
{
get
{
return mFileNames;
}
}
public MainWindow()
{
DataContext = this;
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
ThreadPool.QueueUserWorkItem((x) =>
{
while (true)
{
Dispatcher.BeginInvoke((Action)(() =>
{
mFileNames.Add(new FileInfo("X"));
}));
Thread.Sleep(500);
}
});
}
}
If you run this problem you will notice that the listbox updates every half a second with a new item. Basically the key thing to note is that the ObservableCollection can only be updated from the UI thread so if you refactor the above code you need need to somehow use the Dispatcher of the current UI thread to update it
Related
I am using a Model to select an item in a DataGrid like so:
public ObservableCollection<MyModel> Models{
get {return m_Models; }
}
public MyModel SelectedModel
{
get{ return m_SelectedModel; }
set{
m_SelectedModel = value;
}
NotifyPropertyChanged("SelectedModel");
NotifyPropertyChanged("Models");
}
Those two public properties are in a model that is the data context of the window. The Observable Collection is bound to the data grid and SelectedItem is bound to SelectedModel like so in XAML:
<DataGrid x:Name="MyDataGrid" AutoGenerateColumns="False" IsReadOnly="True" CanUserResizeColumns="True" CanUserAddRows="False" CanUserSortColumns="True"
SelectionMode="Single" SelectionChanged="DataGrid_SelectionChanged" ItemsSource="{Binding Models}"
CanUserResizeRows="False" IsTextSearchEnabled="True" RowHeaderWidth="0" SelectedItem="{Binding SelectedModel, Mode=TwoWay, NotifyOnTargetUpdated=True, NotifyOnSourceUpdated=True}" Grid.ColumnSpan="2" Margin="0,0,-0.4,-0.4">
So, whenever I change the SelectedModel, the SelectedItem in the data grid is updated. This works just fine and I've been able to populate other data based off of the SelectedItem. Indeed, my DataGrid_SelectionChanged event handler gets called upon setting the SelectedModel.
However, whenever I select a new item, I would like the data grid to automatically scroll to show the SelectedItem. I've tried multiple ways to get this to work but to no avail. Here are a few things I've tried (this function is confirmed getting called with debugger):
private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if(MyDataGrid.SelectedItem != null)
{
//All 3 of these have been tried separately and do not work
MyDataGrid.ScrollIntoView(MyDataGrid.SelectedItem);
MyDataGrid.ScrollIntoView(MyDataGrid.Items[MyDataGrid.SelectedIndex]);
MyDataGrid.ScrollIntoView(_model.SelectedModel); //Didn't expect this one to work but just want to show that I'm trying things out here
}
}
I have looked around at other issues with ScrollIntoView and none seem to solve my problem. I'd appreciate any help that would be offered. Thank you.
Not sure why it is not working for you, but it certainly works without any issue. Check out demo application code below:
XAML
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DataGrid x:Name="MyDataGrid" AutoGenerateColumns="False" IsReadOnly="True" CanUserResizeColumns="True" CanUserAddRows="False" CanUserSortColumns="True"
SelectionMode="Single" SelectionChanged="DataGrid_SelectionChanged" ItemsSource="{Binding Items}"
CanUserResizeRows="False" IsTextSearchEnabled="True" RowHeaderWidth="0" SelectedItem="{Binding SelectedItem, Mode=TwoWay, NotifyOnTargetUpdated=True, NotifyOnSourceUpdated=True}"
Grid.ColumnSpan="2" Margin="0,0,-0.4,-0.4">
<DataGrid.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Red" />
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey }" Color="Red" />
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Header="Id" Binding="{Binding Id}" />
<DataGridTextColumn Header="Name" Binding="{Binding Name}" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
Class Files
using System;
using System.ComponentModel;
using System.Linq;
using System.Timers;
using System.Windows;
namespace WpfApplication1
{
public class Model
{
public int Id { get; set; }
public string Name { get; set; }
}
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private readonly Model[] items;
public ViewModel()
{
items = Enumerable.Range(1, 101)
.Select(x => new Model
{
Id = x,
Name = $"Item {x}"
})
.ToArray();
}
public Model[] Items => items;
private Model selectedItem;
public Model SelectedItem
{
get { return selectedItem; }
set
{
selectedItem = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedItem)));
}
}
}
public partial class MainWindow : Window
{
private readonly Timer timer;
private readonly ViewModel model;
private readonly Random random;
public MainWindow()
{
InitializeComponent();
model = new ViewModel();
random = new Random();
timer = new Timer(2000);
DataContext = model;
timer.Elapsed += Timer_Elapsed;
timer.Start();
}
private void Timer_Elapsed(Object sender, ElapsedEventArgs e)
{
var index = random.Next(model.Items.Length);
model.SelectedItem = model.Items[index];
}
private void DataGrid_SelectionChanged(Object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if (MyDataGrid.SelectedItem != null)
{
MyDataGrid.ScrollIntoView(MyDataGrid.SelectedItem);
}
}
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
timer.Stop();
timer.Dispose();
}
}
}
To me it seems, your code is doing something which may be preventing framework from scrolling the grid.
I was able to get ScrollIntoView working but I needed to put the ScrollToView function in the Loaded callback for the data grid. Here's a sample of what fixed the issue:
private void DataGrid_Loaded(object sender, RoutedEventArgs e)
{
if(MyDataGrid.SelectedItem != null)
{
MyDataGrid.ScrollIntoView(MyDataGrid.SelectedItem);
}
}
My guess is that the data grid wasn't fully loaded on the SelectionChanged event and as such, was not scrolling to the proper location.
I have a BindingList binded to a DataGrid, but when I add item to that list, UI is not updating.
Here is a minimal version of code that can reproduce this problem:
MainWindow.xaml
<Window x:Class="WpfTestApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfTestApp1"
mc:Ignorable="d"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<CollectionViewSource x:Key="SessionSource" Source="{Binding Sessions}" />
</Window.Resources>
<Grid>
<DockPanel LastChildFill="True">
<StackPanel DockPanel.Dock="Right">
<Button x:Name="BtnTest" Click="BtnTest_OnClick" Content="Test"></Button>
</StackPanel>
<DataGrid x:Name="DG" DockPanel.Dock="Right" ItemsSource="{Binding Source={StaticResource SessionSource}}">
<DataGrid.Columns>
<DataGridTextColumn x:Name="UserName" Width="Auto" Header="Title" Binding="{Binding Title}"/>
<DataGridTextColumn x:Name="UserAction" Width="Auto" Header="Host" Binding="{Binding Host}"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Grid>
</Window>
MainWindow.xaml.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
namespace WpfTestApp1
{
public class Session : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _title;
public string Title { get => _title; set { _title = value; OnPropertyChanged(); } }
private string _host;
public string Host { get => _host; set { _host = value; OnPropertyChanged(); } }
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public partial class MainWindow : Window
{
public BindingList<Session> Sessions { get; set; }
public MainWindow()
{
InitializeComponent();
Sessions = new BindingList<Session>();
}
private void BtnTest_OnClick(object sender, RoutedEventArgs e)
{
Sessions.Add(new Session(){Title = "test1", Host="test2"});
}
}
}
Tried to implement INotifyPropertyChanged interface in MainWindow class, but seems not working too.
Initialize the Sessions property before calling InitializeComponent:
public partial class MainWindow : Window
{
public BindingList<Session> Sessions { get; } = new BindingList<Session>();
public MainWindow()
{
InitializeComponent();
}
private void BtnTest_OnClick(object sender, RoutedEventArgs e)
{
Sessions.Add(new Session { Title = "test1", Host = "test2" });
}
}
In WPF it isn't necessary to use BindingList, especially not when the element type implements INotifyPropertyChanged. ObservableCollection is more common:
public ObservableCollection<Session> Sessions { get; }
= new ObservableCollection<Session>();
This is my scenario: My DataTemplate of a ListView contains a TextBox and some buttons, one of the buttons is used to select and highlight all of the text in the TextBox. I can find many solutions for select and highlight text in TextBox from code behind, but none of them define the TextBox and the Button in DataTemplate. Anyone can help?
Thanks
You can do something like this below :
XAML :
<Window x:Class="SOWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ListView Width="200" Height="300" ItemsSource="{Binding FriendList}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBox Width="100" Margin="2" Text="{Binding Name}"></TextBox>
<Button Content="Select" Click="Button_Click"></Button>
<Button Content="Delete"></Button>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
Code Behind :
using System.Windows;
using System.Windows.Controls;
namespace SOWPF
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var friendViewModel = new FriendViewModel();
friendViewModel.AddFriends();
this.DataContext = friendViewModel;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var parent = (StackPanel)((sender as Button).Parent);
var children = parent.Children;
foreach(var child in children)
{
if (child.GetType().Equals(typeof(TextBox)))
{
var tb = child as TextBox;
tb.Focus();
tb.SelectAll();
break;
}
}
}
}
}
ViewModel :
using System.Collections.ObjectModel;
namespace SOWPF
{
public class FriendViewModel
{
public ObservableCollection<Friend> FriendList
{ get; set; }
public void AddFriends()
{
FriendList = new ObservableCollection<Friend>();
FriendList.Add(new Friend() { Name = "Arpan" });
FriendList.Add(new Friend() { Name = "Nrup" });
FriendList.Add(new Friend() { Name = "Deba" });
}
}
public class Friend
{
public string Name { get; set; }
}
}
Probably would be a good using an Attached property set on the button, and in the attached code using a code something like written by cvraman.
Using this way you absolutely avoid the code behind structure, and better way to using mvvm
I have been working on this problem for a while and I couldn't figure it out :(
I have a datagrid that dynamically filled with data. Sometimes the data is in the form of date/time.
Now, I need to do two things:
1- Delete the time and show only date (e.g.6/1/2019 12:00:00 AM ---> 6/1/2019)
2- Display datepicker on the cell upon clicking on it.
I googled for solutions and most of solutions are for static datagrid.
I tried resource method but I was not successful.
I appreciate all of your helps
Example of date column in my datagrid:
Something like this, perhaps?
namespace WpfApplication {
public class Thing
{
public string Name { get; set; }
public DateTime Date { get; set; }
}
public partial class MainWindow : Window
{
public MainWindow() {
InitializeComponent();
DataContext = new Thing[] {new Thing {Name = "A", Date = DateTime.Today}};
}
private void DataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e) {
if (e.PropertyName == "Date")
{
var templateColumn = new DataGridTemplateColumn();
templateColumn.Header = "Date";
templateColumn.CellTemplate = (DataTemplate) Resources["DateTemplate"];
templateColumn.CellEditingTemplate = (DataTemplate) Resources["DateEditTemplate"];
e.Column = templateColumn;
}
}
}
}
<Window x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate x:Key="DateTemplate">
<TextBlock Text="{Binding Date, StringFormat='d'}" />
</DataTemplate>
<DataTemplate x:Key="DateEditTemplate">
<DatePicker SelectedDate="{Binding Date, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</Window.Resources>
<Grid>
<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding}" AutoGeneratingColumn="DataGrid_AutoGeneratingColumn" />
</Grid>
</Window>
I have been trying to reproduce this example: http://www.codeproject.com/KB/WPF/TreeViewWithViewModel.aspx
For some reason my treeview isn't showing any data at all and I can't figure out why (I get no errors and don't really know how to debug this decently). This is my code:
MainWindow.XAML
<Window x:Class="Tryout.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Tryout.domain"
Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">
<TreeView Name="treeViewFiles" HorizontalAlignment="Left" Margin="10,10,0,12" Width="200" ClipToBounds="True">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Directory}" ItemsSource="{Binding Children}">
<Label Content="{Binding Path=Name}"/>
</HierarchicalDataTemplate>
<DataTemplate DataType ="{x:Type local:File}">
<TextBlock Text="{Binding Path=Name}" />
</DataTemplate>
</TreeView.Resources>
</TreeView>
MainWindow.XAML.cs
namespace Tryout
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
Directory rootDirectory = new Directory("Root");
rootDirectory.Children.Add(new Directory("Subdirectory 1"));
rootDirectory.Children.Add(new Directory("Subdirectory 2"));
((Directory)rootDirectory.Children[1]).Children.Add(new File("The only file"));
treeViewFiles.ItemsSource = rootDirectory.Children;
}
}
}
File.cs
namespace Tryout.domain
{
public class File : INotifyPropertyChanged
{
public string Name;
public File(String _name)
{
Name = _name;
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
}
Directory.cs
namespace Tryout.domain
{
public class Directory : File
{
public List<File> Children = new List<File>();
public Directory(String _name) : base(_name) { }
}
}
Your File and Directory classes expose their data through fields, not properties, and you can't bind to fields. Change public fields Name and Children to properties and your code will work.