I have a program where I am reading a .csv file with 2 columns, and my model has 2 properties.
I take instances of this model as the file is read and then add them to my ObvervableCollection.
Here is what it looks like:
// My data field
private ObservableCollection<Model> _internalFile = new ObservableCollection<Model>();
// My data accessor/setter
public ObservableCollection<Model> InternalFile { get { return _internalFile; } set { _internalFile = value; } }
Model x = new Model();
while (fileReader.Peek() != -1)
{
// words = read the line, split the line, make a list of columns
var words = fileReader.ReadLine().Split(',').ToList();
if (words[0] != "Name")
{
x.asWord = words[0];
x.asNumber = int.Parse(words[1]);
InternalFile.Add(x);
// InternalFile is a collection of 'Model' objects.
// So created 'Model' placeholder , x, and pile each instance of x
// in the collection to be displayed.
}
}
My XAML looks like this
<Window x:Class="WpfMVVP.WindowView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfMVVP"
Title="Window View" Height="350" Width="525" Background="White">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<DataGrid Grid.Row="0" AutoGenerateColumns="False" CanUserReorderColumns="False" ItemsSource="{Binding Path=InternalFile}">
<DataGrid.Columns>
<DataGridTextColumn Header="As Word" IsReadOnly="True" Width="Auto" Binding="{Binding asWord}" />
<DataGridTextColumn Header="As Number" IsReadOnly="True" Width="Auto" Binding="{Binding asNumber}" />
</DataGrid.Columns>
</DataGrid>
<Label Grid.Row="1" Content="{Binding Status}" />
</Grid>
In the window displayed, I am seeing the columns populate but all the rows are the last row that was read before end of file. It is as if the ObservableCollection is overwriting its previous elements with a new value. I don't know why it's behaving like that.
You're only creating your Model once, and overwriting it each time w/ the last data read. What you want to do is create a new Model for each line read. You can simply move your Model instantiation inside your while loop. (see below)
// My data field
private ObservableCollection<Model> _internalFile = new ObservableCollection<Model>();
// My data accessor/setter
public ObservableCollection<Model> InternalFile { get { return _internalFile; } set { _internalFile = value; } }
while (fileReader.Peek() != -1)
{
Model x = new Model();
// words = read the line, split the line, make a list of columns
var words = fileReader.ReadLine().Split(',').ToList();
if ((words[0] != "Name") && (!String.IsNullOrEmpty(words[0]))
{
x.asWord = words[0];
x.asNumber = int.Parse(words[1]);
InternalFile.Add(x);
// InternalFile is a collection of 'Model' objects.
// So created 'Model' placeholder , x, and pile each instance of x
// in the collection to be displayed.
}
}
Related
How to get selected Cell value from Datagrid?
What information i have looked? Everything where title was "WPF get cell value MVVM".
What i did? 1 step:
<Page x:Class="PDB.UsersView"
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"
xmlns:local="clr-namespace:PDB"
xmlns:PDB ="clr-namespace:PDBapi;assembly=PDBapi"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Title="UsersView"
>
<Page.DataContext>
<PDB:UsersViewModel/>
</Page.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!--Page Header info content-->
<Grid Grid.Row="0">
<TextBlock Text="{Binding ElementName=myGrd, Path=CurrentCell.Column.DisplayIndex}"/>
</Grid>
<!--Datagrid content-->
<DataGrid x:Name="myGrd"
SelectionMode="Single"
SelectionUnit="Cell"
IsReadOnly="True"
Grid.Row="1"
ItemsSource="{Binding Users}"
AutoGenerateColumns="True"
CanUserAddRows="False">
<DataGrid.InputBindings>
<MouseBinding MouseAction="RightClick"
Command="{Binding CellClickCommand}"
CommandParameter="{Binding ElementName=myGrd, Path=CurrentCell}" />
</DataGrid.InputBindings>
</DataGrid>
</Grid>
</Page>
VM:
public UsersViewModel()
{
UserList = new ObservableCollection<User>();
GetUsers();
Users = CollectionViewSource.GetDefaultView(UserList);
CellClickCommand = new RelayParamCommand((data) => GetValue(data));
}
public void GetUsers()
{
User user = new User();
// Users = await user.GetUsers();
UserList.Add(new User
{
Name = "Marius",
Departament = "some",
Tabelis = 5
});
UserList.Add(
new User
{
Name = "Darius",
Departament = "unknown",
Tabelis = 20
});
}
private void GetValue(object data)
{
var some = (DataGridCellInfo)data;
//Returns only number
Console.WriteLine(some.Column?.DisplayIndex.ToString());
}
}
But with this approach i faced 2 issues:
In xaml page i added textblock for testing which text was binded to datagrid currentCell. When i click right mouse button it shows int value correctly. But in my GetValue(object data) function console return null at first right click and from second time returns int value, but value in console is always diferent from textblock value, i have to click two times on same cell to get right cell position. That is completly wrong. How to solve that?
Another issue: How to get real value from currentCell i have binded?
What i did? 2 step:
In xaml i binded datagrid currentCell to VM property CurrentCell="{Binding Cell}"
I got value it was ok, but still it returns only DataGridCellInfo object. I tried to cast to Users object and various things but i failed to get value of cell.
Can someone provide good practice to get cell value from datagrid?
The data grid is bound to a collection of users, so each user is represented by one row, not one cell. That's why CurrentCell returns a DataGridCellInfo
If you want the user, bind to CurrentItem instead.
In XAML:
<DataGrid
ItemsSource="{Binding Users}"
CurrentItem="{Binding SelectedUser}"...
In the view model:
private user selectedUser;
public user SelectedUser
{
get => selectedUser;
set
{
var u = value as user;
selectedUser = u;
}
}
I have a liveChart and am creating checkboxes for each item in a list. This list also has data for each series in liveCharts. How do I bind my dynamically created checkboxes with each individual LiveCharts.LineSeries from my data?
I've created the checkboxes:
<!-- Creating checkboxes by binding to list -->
<ListView ItemsSource="{Binding ElementItemList}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" Width="600">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=ElementName}" />
<CheckBox IsChecked="{Binding Path=ElementIsSelected}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListView>
<!-- Display the chart -->
<Grid Grid.Row="1" x:Name="TestGrid"></Grid>
So I assume that you want to have a CheckBox representing each LineSeries in your SeriesCollection.
So I would have two public properties, one for the SeriesCollection and the other for the list of CheckBox controls.
public SeriesCollection SeriesCollection { get; set; }
public List<CheckBox> CheckBoxCollection { get; set; }
Then following is a function that mimics dynamically creating your LineSeries and CheckBox controls since you didn't provide that code. It is important to have some sort of a connection between the CheckBox controls and your line series, and in this case I decided to set LineSeries.Title and CheckBox.Name the same.
Also note that in order to have the CheckBox do something upon checking/unchecking, you'd need to register two events for each.
public void DynamicallyCreateStuff()
{
SeriesCollection = new SeriesCollection();
CheckBoxCollection = new List<CheckBox>();
var count = 3;
var val1 = new List<double>() { 1, 2, 3 };
var val2 = new List<double>() { 9, 5, 3 };
var val3 = new List<double>() { 1, 4, 9 };
for (int i = 1; i <= count; i++)
{
var name = string.Format("LineSeries{0}", i);
var checkBox = new CheckBox
{
Name = name,
Content = name,
Margin = new Thickness() { Left = 8, Top = 8, Right = 8, Bottom = 8 },
IsChecked = true
};
checkBox.Checked += DynamicCheckBoxChecked;
checkBox.Unchecked += DynamicCheckBoxUnchecked;
CheckBoxCollection.Add(checkBox);
var lineSeries = new LineSeries
{
Title = name
};
if (i == 1)
{
lineSeries.Values = new ChartValues<double>(val1);
}
else if (i == 2)
{
lineSeries.Values = new ChartValues<double>(val2);
}
else if (i == 3)
{
lineSeries.Values = new ChartValues<double>(val3);
}
SeriesCollection.Add(lineSeries);
}
}
In my case, I decided to have the corresponding series become visible/hidden upon clicking the CheckBox, so my check/uncheck methods look like this:
private void DynamicCheckBoxChecked(object sender, EventArgs e)
{
ShowHideSeries(sender, Visibility.Visible);
}
private void DynamicCheckBoxUnchecked(object sender, EventArgs e)
{
ShowHideSeries(sender, Visibility.Collapsed);
}
private void ShowHideSeries(object sender, Visibility visibility)
{
var checkBox = (CheckBox)sender;
var found = SeriesCollection.FirstOrDefault(x => x.Title == checkBox.Name);
if (found != null)
{
var series = (LineSeries)found;
series.Visibility = visibility;
}
}
I didn't use a ViewModel in order to save time and for the sake of simplicity, so my MainWindow constructor looks like this:
public MainWindow()
{
InitializeComponent();
DynamicallyCreateStuff();
DataContext = this;
}
And XAML is pretty bare bones here:
<Window x:Class="SOLineCharts.MainWindow"
....
xmlns:lvc="clr-namespace:LiveCharts.Wpf;assembly=LiveCharts.Wpf"
mc:Ignorable="d"
WindowStartupLocation="CenterScreen"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ItemsControl Grid.Column="0"
ItemsSource="{Binding CheckBoxCollection}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Vertical" ScrollViewer.HorizontalScrollBarVisibility="Disabled"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<lvc:CartesianChart Series="{Binding SeriesCollection}" Grid.Column="1"/>
</Grid>
</Window>
Result:
Upon loading:
Unchecking one check box:
I have a listbox, code below, that I'm populating with 900 items. When I scroll down the list quickly I get an exception of 'An ItemsControl is inconsistent with its items source'. Any ideas as to what is causing the issue?
<ListBox x:Name="FileList" ItemsSource="{Binding Files}" ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.CanContentScroll="True"
MaxHeight="520" >
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<CheckBox x:Name="FileChecked" IsChecked="{Binding Checked}" Grid.Column="0" BorderThickness="1"/>
<Label Content="{Binding Name}" Grid.Column="1" />
<Label Content="{Binding Source}" Grid.Column="2" Style="{StaticResource FileProperties}" />
<Label Content="{Binding Destination}" Grid.Column="3" Style="{StaticResource FileProperties}"/>
<Label Content="{Binding ArchiveLocation}" Grid.Column="4" Style="{StaticResource FileProperties}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
enter image description here
Here is a video of the issue. https://www.screencast.com/t/YUlp24zoXiG
I believe I've found the problem. I've pasted the problematic code below, so that other visitors can benefit from this answer.
// Your code from GitHub unchanged
private async void Preview_Click(object sender, System.Windows.RoutedEventArgs e)
{
vm.Files = new List<InstrumentFile>();
try
{
for (int i = 0; i < InstrumentList.Items.Count; i++)
{
ListBoxItem lbi = (InstrumentList.ItemContainerGenerator.ContainerFromIndex(i)) as ListBoxItem;
ContentPresenter cp = GetFrameworkElementByName<ContentPresenter>(lbi);
DataTemplate dt = InstrumentList.ItemTemplate;
CheckBox cb = (dt.FindName("InstrumentChecked", cp)) as CheckBox;
if (cb.IsChecked == true)
{
List<InstrumentFile> instrumentFiles = new List<InstrumentFile>();
Instrument instrument = ((Instrument)(InstrumentList.Items[i]));
string[] files = Directory.GetFiles(instrument.FileSource);
foreach (string file in files)
{
FileInfo fi = new FileInfo(file);
instrumentFiles.Add(new InstrumentFile()
{
Name = fi.Name,
Source = instrument.FileSource,
Destination = instrument.Destination,
ArchiveLocation = instrument.ArchiveLocation
});
}
if (string.IsNullOrEmpty(instrument.FileExt) == false)
{
IEnumerable<InstrumentFile> filteredFiles = instrumentFiles.Where(f => f.Name.ToUpper().EndsWith(instrument.FileExt.ToUpper()));
if (filteredFiles.Count() > 0)
vm.Files.AddRange(filteredFiles);
}
else
{
if (instrumentFiles.Count > 0)
vm.Files.AddRange(instrumentFiles);
}
}
}
}
catch (Exception ex)
{
await metroWindow.ShowMessageAsync("Exception Encountered", ex.Message, MessageDialogStyle.Affirmative, Helpers.DialogSettings());
}
FileCount.Content = vm.Files.Count.ToString() + " files";
}
Here, you're initializing the Files property in the view model. This causes the data-binding to be updated to an empty list. There's no problem so far. However, you then add things to Files, but these changes are not propagated to the data-binding system because the list in the view model is not an ObservableCollection.
You can fix this problem in a couple of ways. One type of fix is to set the Files property in the view model after you've created and filled the list. The new code would look like this (abbreviated):
private async void Preview_Click(object sender, System.Windows.RoutedEventArgs e)
{
// Create new variable to store the list
var files = new List<InstrumentFile>();
// You do a bunch of things, but you now add items to files, not to vm.Files
files.AddRange(filteredFiles);
// Finally, change Files
vm.Files = files
}
That last line will raise the PropertyChanged event in the view model to update the data-binding, but will do so with the full list of items.
The second kind of fix is to change the type of Files in your view model to ObservableCollection<InstrumentFile>. Any time you make a change to Files, the correct event will be raised to update the data-binding.
I have a grid that has both editable and read-only cells. On selection of cells, if at least one cell is editable (not read-only) I have to enable Cut and Paste icons from the toolbar.
I have bound the IsEnabled property of Cut/Paste icons to a property CanPerformCutPaste.
This is the code that is currently working.
private void dataGrid_SelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e)
{
CanPerformCutPaste = dataGrid.SelectedCells.Any(c => !GetDataGridCell(c, dataGrid).IsReadOnly);
}
public static DataGridCell GetDataGridCell(DataGridCellInfo cellInfo, DataGrid grid)
{
if (cellInfo == null || grid == null)
return null;
grid.ScrollIntoView(cellInfo.Item as DataRowView, cellInfo.Column);
var cellContent = cellInfo.Column.GetCellContent(cellInfo.Item);
if (cellContent != null)
return (DataGridCell)cellContent.Parent;
return null;
}
Is there a better way to achieve this functionality? I don't want this to result in a performance issue for large number of cells.
Edit:
The columns are never set to read-only. Only some cells based on a condition are read-only. Therefore the column property cannot be checked.
You most likely want to check the last selected Cell, instead of checking all the selected cells each time, and besides checking the column property of the selected cell is enough, here a simple example:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button Content="Copy/Past" Grid.Row="0" IsEnabled="{Binding CanPerformCutPaste}"></Button>
<DataGrid ItemsSource="{Binding Items}" x:Name="Dg" Grid.Row="1" AutoGenerateColumns="False" SelectionUnit="Cell" SelectedCellsChanged="Dg_OnSelectedCellsChanged">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" IsReadOnly="True"/>
<DataGridTextColumn Header="Location" Binding="{Binding Location}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
and the SelectedCellsChanged handler:
private void Dg_OnSelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e)
{
if (Dg.SelectedCells.Any())
CanPerformCutPaste = Dg.SelectedCells.Last().Column.IsReadOnly;
}
Ps: I am sure you know that the vm should implements the INotifyPropertyChanged Interface
private bool _canPerformCutPaste ;
public bool CanPerformCutPaste
{
get
{
return _canPerformCutPaste;
}
set
{
if (_canPerformCutPaste == value)
{
return;
}
_canPerformCutPaste = value;
OnPropertyChanged();
}
}
I have a Log object that contains a list of Curve objects. Each curve has a Name property and an array of doubles. I want the Name to be in the column header and the data below it. I have a user control with a datagid. Here is the XAML;
<UserControl x:Class="WellLab.UI.LogViewer"
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"
d:DesignHeight="500" d:DesignWidth="500">
<Grid>
<StackPanel Height="Auto" HorizontalAlignment="Stretch" Margin="0" Name="stackPanel1" VerticalAlignment="Stretch" Width="Auto">
<ToolBarTray Height="26" Name="toolBarTray1" Width="Auto" />
<ScrollViewer Height="Auto" Name="scrollViewer1" Width="Auto" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Visible" CanContentScroll="True" Background="#E6ABA4A4">
<DataGrid AutoGenerateColumns="True" Height="Auto" Name="logDataGrid" Width="Auto" ItemsSource="{Binding}" HorizontalAlignment="Left">
</DataGrid>
</ScrollViewer>
</StackPanel>
</Grid>
In the code behind I have figured out how to create columns and name them, but I have not figured out how to bind the data.
public partial class LogViewer
{
public LogViewer(Log log)
{
InitializeComponent();
foreach (var curve in log.Curves)
{
var data = curve.GetData();
var col = new DataGridTextColumn { Header = curve.Name };
logDataGrid.Columns.Add(col);
}
}
}
I wont even show the code I tried to use to bind the array "data", since nothing even came close. I am sure I am missing something simple, but after hours of searching the web, I have to come begging for an answer.
You need to pivot the log data into a collection of RowDataItem where each row contains a collection of double values, one for each Curve. At the same time you can extract the column names. So the data would end up like this.
public class PivotedLogData : ViewModelBase
{
public PivotedLogData(Log log)
{
ColumnNames = log.Curves.Select(c => c.Name).ToList();
int numRows = log.Curves.Max(c => c.Values.Count);
var items = new List<RowDataItem>(numRows);
for (int i = 0; i < numRows; i++)
{
items.Add(new RowDataItem(
log.Curves.Select(
curve => curve.Values.Count > i
? curve.Values[i]
: (double?) null).ToList()));
}
Items = items;
}
public IList<string> ColumnNames { get; private set; }
public IEnumerable<RowDataItem> Items { get; private set; }
}
public class RowDataItem
{
public RowDataItem(IList<double?> values)
{
Values = values;
}
public IList<double?> Values { get; private set; }
}
Then you would create DataGridTextColumn items as above but with a suitable binding
var pivoted = new PivotedLogData(log);
int columnIndex = 0;
foreach (var name in pivoted .ColumnName)
{
dataGrid.Columns.Add(
new DataGridTextColumn
{
Header = name,
Binding = new Binding(string.Format("Values[{0}]", columnIndex++))
});
}
Now bind the data to the grid
dataGrid.ItemsSource = pivoted.Items;