For a WPF project in need to save the width and the column order of a ListView because these are things the user can change. I guess it is no problem getting the current width but the current position seems to be a bit difficult.
In WinForms there was something like index and displayIndex but I don't see it in WPF.
How is it done?
BTW : Serializing the whole control is not an option.
Edit:
I found some samples using the listView.columns property. But I don't have such a property in my listView
My XAML code is like this:
<ListView>
<ListView.View>
<GridView>
<GridViewColumn>
....
I managed to do that using the Move(…) method of the GridView's Columns collection
If you have the new order stored somehow, you could try:
((GridView)myListView.View).Columns.Move(originalIndex, newIndex);
Edit: This is NOT XAML, but code you should put in the .xaml.cs file
The Columns are always in the same oder that the gridView.Columns Collection is. You can hook into the gridView.CollectionChanged event to react to changes, see also WPF Listview : Column reorder event?
I am using a Behavior to do this. There is a dependency property on the Behavior that is bound to my DataContext. You need a reference to System.Windows.Interactivityto use interactivity.
On my DataContext there is an ObservableCollection of ColumnInfo that I store in my configuration on application exit:
public class ColumnInfo
{
public string HeaderName { get; set; }
public int Width { get; set; }
public int Index { get; set; }
}
In your control add the namespace
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
And the ListView is something like
<ListView ItemsSource="{Binding SomeCollection}">
<ListView.View>
<GridView>
<i:Interaction.Behaviors>
<b:GridViewColumnBehavior Columns="{Binding Columns}" />
</i:Interaction.Behaviors>
</GridView>
</ListView.View>
</ListView>
The Behavior I'm using (parts of it)
public class GridViewColumnBehavior : Behavior<GridView>
{
public ObservableCollection<ColumnInfo> Columns
{
get { return (ObservableCollection<ColumnInfo>)GetValue(ColumnsProperty); }
set { SetValue(ColumnsProperty, value); }
}
public static readonly DependencyProperty ColumnsProperty =
DependencyProperty.Register("Columns", typeof(ObservableCollection<ColumnInfo>), typeof(GridViewColumnBehavior), new PropertyMetadata(null, new PropertyChangedCallback(Columns_Changed)));
private static void Columns_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var b = d as GridViewColumnBehavior;
if (b == null) return;
b.SetupColumns(e.NewValue as ObservableCollection<Column>);
}
public void SetupColumns(ObservableCollection<Column> oldColumns)
{
if(oldColumns != null)
{
oldColumns.CollectionChanged -= Columns_CollectionChanged;
}
if ((Columns?.Count ?? 0) == 0) return;
AssociatedObject.Columns.Clear();
foreach (var column in Columns.OrderBy(c => c.Index))
{
AddColumn(column);
}
Columns.CollectionChanged += Columns_CollectionChanged;
}
private void Columns_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
var lookup = AssociatedObject.Columns.Select((c, i) => new { Index = i, Element = c.Header.ToString() }).ToLookup(ci => ci.Element, ci => ci.Index);
foreach (var c in Columns)
{
// store the index in the Model (ColumnInfo)
c.Index = lookup[c.HeaderName].FirstOrDefault();
}
}
}
Enjoy!
Related
I try to create a DataGrid for WPF / MVVM which allows to manually select one ore more items from ViewModel code.
As usual the DataGrid should be able to bind its ItemsSource to a List / ObservableCollection. The new part is that it should maintain another bindable list, the SelectedItemsList. Each item added to this list should immediately be selected in the DataGrid.
I found this solution on Stackoverflow: There the DataGrid is extended to hold a Property / DependencyProperty for the SelectedItemsList:
public class CustomDataGrid : DataGrid
{
public CustomDataGrid()
{
this.SelectionChanged += CustomDataGrid_SelectionChanged;
}
private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
this.SelectedItemsList = this.SelectedItems;
}
public IList SelectedItemsList
{
get { return (IList)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public static readonly DependencyProperty SelectedItemsListProperty =
DependencyProperty.Register("SelectedItemsList",
typeof(IList),
typeof(CustomDataGrid),
new PropertyMetadata(null));
}
In the View/XAML this property is bound to the ViewModel:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ucc:CustomDataGrid Grid.Row="0"
ItemsSource="{Binding DataGridItems}"
SelectionMode="Extended"
AlternatingRowBackground="Beige"
SelectionUnit="FullRow"
IsReadOnly="True"
SelectedItemsList="{Binding DataGridSelectedItems,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
<Button Grid.Row="1"
Margin="5"
HorizontalAlignment="Center"
Content="Select some rows"
Command="{Binding CmdSelectSomeRows}"/>
</Grid>
The ViewModel also implements the command CmdSelectSomeRows which selects some rows for testing. The ViewModel of the test application looks like this:
public class CustomDataGridViewModel : ObservableObject
{
public IList DataGridSelectedItems
{
get { return dataGridSelectedItems; }
set
{
dataGridSelectedItems = value;
OnPropertyChanged(nameof(DataGridSelectedItems));
}
}
public ICommand CmdSelectSomeRows { get; }
public ObservableCollection<ExamplePersonModel> DataGridItems { get; private set; }
public CustomDataGridViewModel()
{
// Create some example items
DataGridItems = new ObservableCollection<ExamplePersonModel>();
for (int i = 0; i < 10; i++)
{
DataGridItems.Add(new ExamplePersonModel
{
Name = $"Test {i}",
Age = i * 22
});
}
CmdSelectSomeRows = new RelayCommand(() =>
{
if (DataGridSelectedItems == null)
{
DataGridSelectedItems = new ObservableCollection<ExamplePersonModel>();
}
else
{
DataGridSelectedItems.Clear();
}
DataGridSelectedItems.Add(DataGridItems[0]);
DataGridSelectedItems.Add(DataGridItems[1]);
DataGridSelectedItems.Add(DataGridItems[4]);
DataGridSelectedItems.Add(DataGridItems[6]);
}, () => true);
}
private IList dataGridSelectedItems = new ArrayList();
}
This works, but only partially: After application start when items are added to the SelectedItemsList from ViewModel, they are not displayed as selected rows in the DataGrid. To get it to work I must first select some rows with the mouse. When I then add items to the SelectedItemsList from ViewModel these are displayed selected – as I want it.
How can I achieve this without having to first select some rows with the mouse?
You should subscribe to the Loaded event in your CustomDataGrid and initialize the SelectedItems of the Grid (since you never entered the SelectionChangedEvent, there is no link between the SelectedItemsList and the SelectedItems of your DataGrid.
private bool isSelectionInitialization = false;
private void CustomDataGrid_Loaded(object sender, RoutedEventArgs e)
{
this.isSelectionInitialization = true;
foreach (var item in this.SelectedItemsList)
{
this.SelectedItems.Clear();
this.SelectedItems.Add(item);
}
this.isSelectionInitialization = false;
}
and the SelectionChanged event handler has to be modified like this:
private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!this.isSelectionInitialization)
{
this.SelectedItemsList = this.SelectedItems;
}
else
{
//Initialization from the ViewModel
}
}
Note that while this will fix your problem, this won't be a true synchronization as it will only copy the items from the ViewModel at the beginning.
If you need to change the items in the ViewModel at a later time and have it reflected in the selection let me know and I will edit my answer.
Edit: Solution to have a "true" synchronization
I created a class inheriting from DataGrid like you did.
You will need to add the using
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
public class CustomDataGrid : DataGrid
{
public CustomDataGrid()
{
this.SelectionChanged += CustomDataGrid_SelectionChanged;
this.Loaded += CustomDataGrid_Loaded;
}
private void CustomDataGrid_Loaded(object sender, RoutedEventArgs e)
{
//Can't do it in the constructor as the bound values won't be initialized
//If it is expected for the bound collection to be null initially, you could subscribe to the change of the
//dependency in order to subscribe to the collectionChanged event on the first non null value
this.SelectedItemsList.CollectionChanged += SelectedItemsList_CollectionChanged;
//We call the update in case we have already some items in the VM collection
this.UpdateUIWithSelectedItemsFromVm();
if(this.SelectedItems.Count != 0)
{
//Otherwise the items won't be as visible unless you change the style (this part is not required)
this.Focus();
}
else
{
//No focus
}
}
private void SelectedItemsList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
this.UpdateUIWithSelectedItemsFromVm();
}
private void UpdateUIWithSelectedItemsFromVm()
{
if (!this.isSelectionChangeFromUI)
{
this.isSelectionChangeFromViewModel = true;
this.SelectedItems.Clear();
if (this.SelectedItemsList == null)
{
//Nothing to do, we just cleared all the selections
}
else
{
if (this.SelectedItemsList is IList iListFromVM)
foreach (var item in iListFromVM)
{
this.SelectedItems.Add(item);
}
}
this.isSelectionChangeFromViewModel = false;
}
else
{
//Nothing to do, the change is coming from the SelectionChanged event
}
}
private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
//If your collection allow suspension of notifications, it would be a good idea to add a check here in order to use it
if(!this.isSelectionChangeFromViewModel)
{
this.isSelectionChangeFromUI = true;
if (this.SelectedItemsList is IList iListFromVM)
{
iListFromVM.Clear();
foreach (var item in SelectedItems)
{
iListFromVM.Add(item);
}
}
else
{
throw new InvalidOperationException("The bound collection must inherit from IList");
}
this.isSelectionChangeFromUI = false;
}
else
{
//Nothing to do, the change is comming from the bound collection so no need to update it
}
}
private bool isSelectionChangeFromUI = false;
private bool isSelectionChangeFromViewModel = false;
public INotifyCollectionChanged SelectedItemsList
{
get { return (INotifyCollectionChanged)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public static readonly DependencyProperty SelectedItemsListProperty =
DependencyProperty.Register(nameof(SelectedItemsList),
typeof(INotifyCollectionChanged),
typeof(CustomDataGrid),
new PropertyMetadata(null));
}
You will have to initialize the DataGridSelectedItems earlier or you there will be a null exception when trying to subscribe to the collectionChanged event.
/// <summary>
/// I removed the notify property changed from your example as it probably isn't necessary unless you really intended to create a new Collection at some point instead of just clearing the items
/// (In this case you will have to adapt the code for the synchronization of CustomDataGrid so that it subscribe to the collectionChanged event of the new collection)
/// </summary>
public ObservableCollection<ExamplePersonModel> DataGridSelectedItems { get; set; } = new ObservableCollection<ExamplePersonModel>();
I didn't try all the edge cases but this should give you a good start and I added some directions as to how to improve it. Let me know if some parts of the code aren't clear and I will try to add some comments.
So I'm new to WPF as I'm more familiar with WinForms, But for the sake of drawing performance and good looking UI I switched to WPF, I have no experience in XAML but I'm working my things out.
I have a ListView which works as a playlist for my Media Player App. Adding Multiple Data in one line of multiple columns wasn't a problem in WinForms, I just had to add a ListViewItem and fill it's SubItems , but in WPF it's a problem, the ListViewItem doesn't have SubItems property nor the ListView, I tried multiple questions from Stack Overflow and other website which didn't help me , and it was all about DisplayMemberBinding but I still can't / don't know how to reference it in my code.
XAML for ListView:
<ListView x:Name="Playlist_Main" Margin="0" ItemsSource="{Binding SourceCollection}">
<ListView.View>
<GridView>
<GridViewColumn Header="#" DisplayMemberBinding="{Binding Num}"/>
<GridViewColumn Header="Title" DisplayMemberBinding="{Binding Title}"/>
<GridViewColumn Header="Artist" DisplayMemberBinding="{Binding Artist}"/>
<GridViewColumn Header="Album" DisplayMemberBinding="{Binding Album}"/>
<GridViewColumn Header="Year" DisplayMemberBinding="{Binding Year}"/>
<GridViewColumn Header="Track Num" DisplayMemberBinding="{Binding Track}"/>
</GridView>
</ListView.View>
</ListView>
Main Code
Playlist_Main.Items.Add(New ListViewItem({Playlist_Main.Items.Count + 1, Info(0), Info(1), Info(2), Info(3), Info(4)}))
The bindings in your ListView / GridView require an item type that exposes properties for Num, Title, and so on. You have to expose a collection of that item type and assign or bind it to the ItemsSource property.
Create a model for an item in your playlist. The following example implements the INotifyPropertyChanged interface that enables bindings to update through the PropertyChanged event if a property value changes. If your properties are read-only or you do not need to update values at runtime, you do not have to implement it.
public class PlaylistItem : INotifyPropertyChanged
{
private int _num;
private string _title;
private string _artist;
private string _album;
private int _year;
private int _track;
public PlaylistItem(int num, string title, string artist, string album, int year, int track)
{
Num = num;
Title = title;
Artist = artist;
Album = album;
Year = year;
Track = track;
}
public int Num
{
get => _num;
set
{
if (_num == value)
return;
_num = value;
OnPropertyChanged();
}
}
public string Title
{
get => _title;
set
{
if (_title == value)
return;
_title = value;
OnPropertyChanged();
}
}
public string Artist
{
get => _artist;
set
{
if (_artist == value)
return;
_artist = value;
OnPropertyChanged();
}
}
public string Album
{
get => _album;
set
{
if (_album == value)
return;
_album = value;
OnPropertyChanged();
}
}
public int Year
{
get => _year;
set
{
if (_year == value)
return;
_year = value;
OnPropertyChanged();
}
}
public int Track
{
get => _track;
set
{
if (_track == value)
return;
_track = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
In a code-behind approach, you could create a collection of PlaylistItems. Use an ObservableCollection<T> that implements the INotifyCollectionChanged interface, if you want to reflect changes to the collection in the user interface, e.g. adding or removing items. The ObservableCollection<T> does that automatically through the CollectionChanged event. If your list is not modified at runtime, you can use any other collection.
var playlistItems = new ObservableCollection<PlaylistItem>();
playlistItems.Add(new PlaylistItem(1, "Enter Sandman", "Metallica", "Metallica", 1991, 1));
// ...add other playlist items.
You could assign this collection directly to the ListView e.g. in the constructor.
public MainWindow()
{
var playlistItems = // ...create the items collection or load them from somewhere.
Playlist_Main.ItemsSource = playlistItems;
}
A different approach is creating a public property in your code-behind. I assume it is the MainWindow.
public partial class MainWindow
{
public MainWindow()
{
PlaylistItems = // ...create the items collection or load them from somewhere.
}
public ObservableCollection<PlaylistItem> PlaylistItems { get; }
}
You would bind this collection in XAML using a RelativeSource binding to the window.
<ListView ItemsSource="{Binding Tracks, RelativeSource={RelativeSource AncestorType={x:Type local:MainWindow}}}">
As you can see, there are multiple approaches, even more. The best, in my opinion, would be to use the MVVM pattern. For that, you would create a view model for your main window that contains the collection.
public class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
{
PlaylistItems = // ...create the items collection or load them from somewhere.
}
public ObservableCollection<PlaylistItem> PlaylistItems { get; }
// ...other properties and methods.
}
Next you would set an instance of this view model as DataContext of your window.
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
Then you can bind to the collection like this. The data context (MainViewModel) is inherited automatically.
<ListView ItemsSource="{Binding PlaylistItems}">
This pattern helps you to separate the user interface from the data and business logic. As you can see, there are no references from the view model to the view, only properties that expose data that can be bound.
Further resources for learning:
Data binding overview in WPF
How to: Create and Bind to an ObservableCollection
Is there any way of feeding the columns of a Datagrid to a GridViewRowPresenter ?
It cannot be done directly, as one uses DataGridColumn and the other GridViewColumn so this doesn't work:
<GridViewRowPresenter Columns="{Binding ElementName=myDataGrid, Path=Columns, Mode=OneWay}" />
I haven't tried this, but something like this should work:
public class MyGridViewRowPresenter : GridViewRowPresenter
{
public static readonly DependencyProperty NumberOfColumnsProperty = DependencyProperty.Register("NumberOfColumns", typeof(int), typeof(MyGridViewRowPresenter), new PropertyMetadata(0));
public int NumberOfColumns
{
get { return (int)GetValue(NumberOfColumnsProperty); }
set { SetValue(NumberOfColumnsProperty, value); }
}
public override void EndInit()
{
base.EndInit();
for (var i = 0; i < NumberOfColumns; i++)
{
Columns.Add(new GridViewColumn());
}
}
}
usage
<local:MyGridViewRowPresenter NumberOfColumns="{Binding ElementName=myDataGrid, Path=Columns.Count, Mode=OneWay}" />
I did something similar to a standard grid, so instead of doing row or column definitions and then add column and rows, i would just say columns=somenumber and that would do it.
I need to update the list of downloads when the progress has been changed.
XAML:
<ListView ItemsSource="{Binding UiList}" x:Name="MyListView">
<ListView.View>
<GridView>
<GridViewColumn Header="Title"/>
<GridViewColumn Header="Progress"/>
</GridView>
</ListView.View>
</ListView>
ViewModel that creates an instance of Logic class and updates the content of ListView:
class MainWindowViewModel : ViewModelBase
{
private readonly Logic _logic;
public List<Model> UiList { get; set; }
public MainWindowViewModel()
{
_logic = new Logic();
_logic.Update += LogicUpdate;
Start = new RelayCommand(() =>
{
var worker = new BackgroundWorker();
worker.DoWork += (sender, args) => _logic.Start();
worker.RunWorkerAsync();
});
}
void LogicUpdate(object sender, EventArgs e)
{
UiList = _logic.List;
RaisePropertyChanged("UiList");
}
public ICommand Start { get; set; }
}
Logic:
public class Logic
{
readonly List<Model> _list = new List<Model>();
public event EventHandler Update;
public List<Model> List
{
get { return _list; }
}
public void Start()
{
for (int i = 0; i < 100; i++)
{
_list.Clear();
_list.Add(new Model{Progress = i, Title = "title1"});
_list.Add(new Model { Progress = i, Title = "title2" });
var time = DateTime.Now.AddSeconds(2);
while (time > DateTime.Now)
{ }
Update(this, EventArgs.Empty);
}
}
}
The code above would not update UI. I know two way how to fix this:
In xaml codebehind call: Application.Current.Dispatcher.Invoke(new Action(() => MyListView.Items.Refresh()));
In ViewModel change List<> to ICollectionView and use Application.Current.Dispatcher.Invoke(new Action(() => UiList.Refresh())); after the list has been updated.
Both ways cause the problem: the ListView blinks and Popup that should be open on user demand always closes after each "refresh":
<Popup Name="Menu" StaysOpen="False">
I can replace the popup with another control or panel, but I need its possibility to be out of main window's border (like on screen). But I believe that WPF has another way to update the content of ListView (without blinks).
PS: Sorry for long question, but I do not know how to describe it more briefly...
I think the reason this line doesn't work:
RaisePropertyChanged("UiList");
Is because you haven't actually changed the list. You cleared it and repopulated it, but it's still the reference to the same list. I'd be interested to see what happens if, instead of clearing your list and repopulating, you actually created a new list. I think that should update your ListView as you expected. Whether or not it has an effect on your popup, I don't know.
I've found the answer here: How do I update an existing element of an ObservableCollection?
ObservableCollection is a partial solution. ObservableCollection rises CollectionChanged event only when collection changes (items added, removed, etc.) To support updates of existent items, each object inside the collection (Model class in my case) must implement the INotifyPropertyChanged interface.
// I used this parent (ViewModelBase) for quick testing because it implements INotifyPropertyChanged
public class Model : ViewModelBase
{
private int _progress;
public int Progress
{
get { return _progress; }
set
{
_progress = value;
RaisePropertyChanged("Progress");
}
}
public string Title { get; set; }
}
I've tried to search for an answer to this but I'm not having any luck. Basically I have a listview that is bound to a collection returned from a view model. I bind the selected item of the list view to a property in my listview in order to perform validation to ensure that an item is selected. The problem is that sometimes I want to load this listview with one of the items already selected. I was hoping to be able to set the property on my view model with the object I want selected and have it automatically select that item. This is not happening. My listview loads without an item selected. I can successfully set the selected index to the 0th index so why shouldn't I be able to set the selected value. The list view is in single selection mode.
Here's the pertinent code from my list view
<ListView Name="listView1" ItemsSource="{Binding Path=AvailableStyles}" SelectionMode="Single">
<ListView.SelectedItem>
<Binding Path="SelectedStyle" ValidatesOnDataErrors="True" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" BindingGroupName="StyleBinding" >
</Binding>
</ListView.SelectedItem>
<ListView.View>
<GridView>
<GridViewColumn Header="StyleImage">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Image Source="800.jpg"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Style Code" DisplayMemberBinding="{Binding StyleCode}"/>
<GridViewColumn Header="Style Name" DisplayMemberBinding="{Binding StyleName}"/>
</GridView>
</ListView.View>
</ListView>
And here is the pertinent code from my view model
public class StyleChooserController : BaseController, IDataErrorInfo, INotifyPropertyChanged
{
private IList<Style> availableStyles;
private Style selectedStyle;
public IList<Style> AvailableStyles
{
get { return availableStyles; }
set
{
if (value == availableStyles)
return;
availableStyles = value;
OnPropertyChanged("AvailableStyles");
}
}
public Style SelectedStyle
{
get { return selectedStyle; }
set
{
//if (value == selectedStyle)
// return;
selectedStyle = value;
OnPropertyChanged("SelectedStyle");
}
}
public StyleChooserController()
{
AvailableStyles = StyleService.GetStyleByVenue(1);
if (ApplicationContext.CurrentStyle != null)
{
SelectedStyle = ApplicationContext.CurrentStyle;
}
}
public string Error
{
get { return null; }
}
public string this[string columnName]
{
get
{
string error = string.Empty;
if (columnName == "SelectedStyle")
{
if (SelectedStyle == null)
{
error = "required";
}
}
return error;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
}
I should note that the "Style" referenced here has nothign to do with WPF. It's a business object. I'm really looking for a solution that doesn't break the MVVM pattern, but I'd be willing to just get something functioning. I've attempted to loop through the Listview.Items list just to set it manually but it's always empty when I try. Any help is appreciated.
Edit: I updated the code to use INotifyPropertyChanged. It's still not working. Any other suggestions
2nd Edit: I added UpdateSourceTrigger="PropertyChanged". That still did not work.
Thanks
Your problem is most likely caused because your SelectedItem Style is a different Style instance than the matching one in the AvailableStyles in the ItemsSource.
What you need to do is provide your specific definition of equality in your Style class:
public class Style: IEquatable<Style>
{
public string StyleCode { get; set; }
public string StyleName { get; set; }
public virtual bool Equals(Style other)
{
return this.StyleCode == other.StyleCode;
}
public override bool Equals(object obj)
{
return Equals(obj as Style);
}
}
Hmm... it looks like you forgot to implement INotifyPropertyChanged for the SelectedStyle property...