Grouped listview crashes on second refresh - c#

I've got a grouped Xamarin ListView, and found it troublesome updating the values within the ListView.
ViewModel;
public class MainViewModel
{
private ObservableCollection<ObservableGroupCollection<string, TaskItem>> _userTasksGrouped;
public ObservableCollection<ObservableGroupCollection<string, TaskItem>> UserTasksGrouped
{
get => _userTasksGrouped;
set
{
_userTasksGrouped = value;
OnPropertyChanged();
}
}
private void PopulateMyTasks()
{
UserTasks.Clear();
foreach (var task in MyTasks)
{
UserTasks.Add(new TaskItem(task));
}
DoTaskGrouping();
}
private void DoTaskGrouping()
{
UserTasksGrouped.Clear();
var groupedData =
UserTasks
.OrderBy(ut => ut.Name)
.GroupBy(ut => ut.StateEnum.ToString())
.Select(ut => new ObservableGroupCollection<string, TaskItem>(ut))
.ToList();
foreach (var group in groupedData)
UserTasksGrouped.Add(group);
}
public void UpdateTaskItem(TaskChanged taskChanged)
{
// update listing
var usertask = UserTasksGrouped.First(utg => utg.Any(ut => ut.Task.Id == taskChanged.Task.Id))
.First(t => t.Task.Id == taskChanged.Task.Id);
if (usertask != null)
{
usertask.Task = taskChanged.Task;
}
else
{
// something else
}
DoTaskGrouping();
}
public MainViewModel()
{
UserTasks = new ObservableCollection<TaskItem>();
UserTasksGrouped = new ObservableCollection<ObservableGroupCollection<string, TaskItem>>();
}
}
XAML;
<ListView
IsGroupingEnabled="true"
ItemsSource="{Binding UserTasksGrouped}"
GroupDisplayBinding="{Binding Key}"
GroupShortNameBinding="{Binding Key}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.View>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="50*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<BoxView
Grid.Row="0" Grid.RowSpan="2"
Grid.Column="0" Grid.ColumnSpan="2"
MinimumHeightRequest="16"
MinimumWidthRequest="16"
Margin="0"
Color="{Binding StatusColour}" />
<Label
Grid.Row="0" Grid.Column="2"
Text="{Binding Name}"
Style="{StaticResource LabelStyle}"
WidthRequest="200" />
</Grid>
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Task Item:
public class TaskItem : INotifyPropertyChanged
{
private MyTask _task;
public MyTask Task
{
get => _task;
set
{
_task = value;
Name = value.Name;
OnPropertyChanged();
}
}
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged();
}
}
public string StatusColour => "Green"
}
The problem when I call DoTaskGrouping() for the second time to force a refresh the App crashes with an UnhandledException. I managed to track this down to a "An item with the same key has already been added" error, so I added the ViewCell.View thinking it might fix things. Please help, I have a few questions;
How do I fix the error "An item with same key has already been added" and successfully refresh my grouped listing.
Do Observable collections within observable collections update themselves. Meaning if the properties or what their grouped by changes, so does the grouping? Meaning an item can move between groups?
Is my approach sound from a MVVM perspective? TaskItem could do with cleaning up.

Fixed by removing from the MainViewModel
public ObservableCollection<ObservableGroupCollection<string, TaskItem>> UserTasksGrouped
DoTaskGrouping() building the collection afresh each time;
UserTasksGrouped = new ObservableCollection<ObservableGroupCollection<string, TaskItem>>(groupedData);

Related

How can you bind a Label to a function result in Xamarin.Forms

I'm trying to bind a Label to the result of the GetPlayCount() function call. The other bindings, for Name and Category, are working as expected, but there is no output for the third label
XAML:
<ListView ItemsSource="{Binding Games}"
HasUnevenRows="true"
HeightRequest="200"
SeparatorVisibility="Default">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.View>
<Grid Margin="0" Padding="0" RowSpacing="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Margin="0" Text="{Binding Name}"/>
<Label Grid.Column="1" Margin="0" Text="{Binding Category}"/>
<!--This following Label is the one not binding -->
<Label Grid.Column="2" Margin="0" Text="{Binding GetPlayCount}" />
</Grid>
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Code Behind:
public partial class CollectionPage : ContentPage
{
CollectionViewModel collectionView = new CollectionViewModel();
public CollectionPage()
{
InitializeComponent();
BindingContext = collectionView;
}
}
ViewModel:
public class CollectionViewModel : INotifyPropertyChanged
{
private ObservableCollection<Game> games;
public ObservableCollection<Game> Games
{
get { return games; }
set
{
games = value;
OnPropertyChanged("Games");
}
}
public CollectionViewModel()
{
GetGames();
}
public async void GetGames()
{
var restService = new RestService();
Games = new ObservableCollection<Game>(await restService.GetGamesAsync());
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Model:
public class Game
{
public string Name { get; set; }
public string Category { get; set; }
public async Task<int> GetPlayCount()
{
return (await new RestService().GetGamesAsync()).Where(result => result.Name == this.Name).Count();
}
}
You can bind only to the property. You may call that function from the property getter. However it is not possible to bind to the function. The app doesn't know when your function is updated, so binding wouldn't make much sense. For the property you can call PropertyChanged to signal that the property has a new value.
I will talk with code:
[NotMapped]
public decimal TotalPrice { get =>GetTotalPrice(); }
private decimal GetTotalPrice()
{
decimal result = 0;
foreach(var dpo in DetailPurchaseOrder)
{
result = result + dpo.GetTotalPurchasePrice();
}
return result;
}

WPF Application MVVM - Using OneWay binding on Textbox, Property becomes null after getting data again

I have an WPF application using MVVM.
I have a DataGrid bound to an ObservableCollection and a TextBox bound to the DataGrid SelectedItem, so when I click an item in the DataGrid, the TextBox is populated.
I also have a Button using Command and CommandParameter and using RelayCommand to check if the TextBox is empty and then disabling the Button.
That all works great, if I use UpdateSourceTrigger=PropertyChanged. The thing I don't like is because of the binding, if the user changes the text in the TextBox, the DataGrid record is edited. If the user then changes their mind about changing the record, and clicks somewhere else, the record in the DataGrid still shows the edited text.
What I have tried is using Mode=OneWay on the TextBox binding, which works in that it doesn't update the DataGrid record. After the data is saved to the database, I need to manually refresh the DataGrid to show the changes.
The code I have in my code behind is the DataGrid's SelectionChanged event which sets a property on the ViewModel to the selected item.
So in order to show the new changes, I thought adding a call to my GetCategories again after the changes would work. However when the code executes OnPropertyChanged("ReceivedCategories"), my CurrentCategory property becomes null.
My code:
CategoryModel.cs
public class CategoryModel
{
public int CategoryID { get; set; }
public string Description { get; set; }
readonly SalesLinkerDataContext _dbContext = new SalesLinkerDataContext();
public ObservableCollection<CategoryModel> GetCategories()
{
var result = _dbContext.tblSalesCategories.ToList();
List<CategoryModel> categoriesList = result.Select(item => new CategoryModel
{
CategoryID = item.CategoryID,
Description = item.Description.Trim()
}).ToList();
return new ObservableCollection<CategoryModel>(categoriesList);
}
internal bool UpdateCategory(int id, string description)
{
if (_dbContext.tblSalesCategories.Any(x => x.Description == description))
{
MessageBox.Show("A category with the same name already exists.");
return false;
}
try
{
var category = (from a in _dbContext.tblSalesCategories
where a.CategoryID == id
select a).FirstOrDefault();
if (category != null)
{
category.Description = description;
_dbContext.SubmitChanges();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return false;
}
return true;
}
internal bool AddCategory(string description)
{
if (_dbContext.tblSalesCategories.Any(x => x.Description == description))
{
MessageBox.Show("A category with the same name already exists.");
return false;
}
var newCategory = new tblSalesCategory();
newCategory.Description = description;
try
{
_dbContext.tblSalesCategories.InsertOnSubmit(newCategory);
_dbContext.SubmitChanges();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return false;
}
return true;
}
internal bool DeleteCategory(int id)
{
var result = _dbContext.tblSalesCategories.FirstOrDefault(x => x.CategoryID == id);
try
{
if (result != null)
{
_dbContext.tblSalesCategories.DeleteOnSubmit(result);
_dbContext.SubmitChanges();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return false;
}
return true;
}
}
CategoriesViewModel.cs
public class CategoriesViewModel : ViewModelBase, IPageViewModel
{
public CategoryModel CurrentCategory = new CategoryModel();
public ObservableCollection<CategoryModel> Categories = new ObservableCollection<CategoryModel>();
public RelayCommand GetCategoriesRelay;
public RelayCommand UpdateCategoryRelay;
public RelayCommand AddCategoryRelay;
public RelayCommand DeleteCategoryRelay;
#region Get Categories Command
public ICommand GetCategoriesCommand
{
get
{
GetCategoriesRelay = new RelayCommand(p => GetCategories(),
p => CanGetCategories());
return GetCategoriesRelay;
}
}
private bool CanGetCategories()
{
return true;
}
private void GetCategories()
{
Categories = CurrentCategory.GetCategories();
ReceivedCategories = Categories;
}
#endregion
#region Update Category Command
public ICommand UpdateCategoryCommand
{
get
{
UpdateCategoryRelay = new RelayCommand(p => UpdateCategory((string) p),
p => CanUpdateCategory());
return UpdateCategoryRelay;
}
}
public bool CanUpdateCategory()
{
return !String.IsNullOrWhiteSpace(Description);
}
public void UpdateCategory(string description)
{
if (CurrentCategory.UpdateCategory(CurrentCategory.CategoryID, description))
{
GetCategories();
}
}
#endregion
#region Add Category Command
public ICommand AddCategoryCommand
{
get
{
AddCategoryRelay = new RelayCommand(p => AddCategory((string) p),
p => CanAddCategory());
return AddCategoryRelay;
}
}
private bool CanAddCategory()
{
return !String.IsNullOrWhiteSpace(Description);
}
private void AddCategory(string description)
{
if (CurrentCategory.AddCategory(description))
GetCategories();
}
#endregion
#region Delete Category Command
public ICommand DeleteCategoryCommand
{
get
{
DeleteCategoryRelay = new RelayCommand(p => DeleteCategory((int) p),
p => CanDeleteCategory());
return DeleteCategoryRelay;
}
}
private bool CanDeleteCategory()
{
return true;
}
private void DeleteCategory(int id)
{
if (CurrentCategory.DeleteCategory(id))
GetCategories();
}
#endregion
/// <summary>
/// Describes the name that will be used for the menu option
/// </summary>
public string Name
{
get { return "Manage Categories"; }
}
public string Description
{
get
{
return CurrentCategory.Description;
}
set
{
CurrentCategory.Description = value;
OnPropertyChanged("Description");
}
}
public ObservableCollection<CategoryModel> ReceivedCategories
{
get { return Categories; }
set
{
Categories = value;
OnPropertyChanged("ReceivedCategories");
}
}
}
CategoryView.xaml
<UserControl x:Class="SalesLinker.CategoriesView"
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:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="600" Background="White">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding GetCategoriesCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="45"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Margin="20,0,0,0" FontSize="20" HorizontalAlignment="Center" Content="Categories"/>
<DataGrid x:Name="LstCategories" Grid.Column="0" Grid.Row="1" AutoGenerateColumns="false"
ItemsSource="{Binding Path=ReceivedCategories, Mode=TwoWay}" SelectionChanged="Selector_OnSelectionChanged"
HorizontalScrollBarVisibility="Disabled" GridLinesVisibility="None"
CanUserAddRows="False" CanUserDeleteRows="False" CanUserSortColumns="True" Background="White">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=Description}" IsReadOnly="True" Header="Description" Width="300" />
</DataGrid.Columns>
</DataGrid>
<Button Command="{Binding AddCategoryCommand}" Grid.Column="1" Grid.Row="1" VerticalAlignment="Top" Height="50" Width="50" Margin="0,20,0,0" Background="Transparent" BorderThickness="0" BorderBrush="Transparent"
CommandParameter="{Binding ElementName=TbDescription, Path=Text}">
<Image Source="/Images/Plus.png"/>
</Button>
<Button Command="{Binding DeleteCategoryCommand}" Grid.Column="1" Grid.Row="1" VerticalAlignment="Top" Height="50" Width="50" Margin="0,75,0,0" Background="Transparent" BorderThickness="0" BorderBrush="Transparent"
CommandParameter="{Binding SelectedItem.CategoryID, ElementName=LstCategories, Mode=OneWay }">
<Image Source="/Images/Minus.png"/>
</Button>
<Grid Grid.Row="1" Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="75"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Row="0" Grid.Column="0" Content="Description:"/>
<TextBox x:Name="TbDescription" DataContext="CategoryModel" Grid.Row="0"
Grid.Column="1" Width="250" Height="Auto" VerticalAlignment="Center"
HorizontalAlignment="Left" Margin="10,0,0,0"
Text="{Binding SelectedItem.Description, ElementName=LstCategories, Mode=OneWay}"/>
<Button Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left" Margin="10,0,0,0"
Height="20" Width="120" Content="Update Description"
Command="{Binding UpdateCategoryCommand}"
CommandParameter="{Binding ElementName=TbDescription, Path=Text}" />
</Grid>
</Grid>
And I have also just noticed that using Mode=OneWay on the TextBox breaks my CanExecute pieces of code as well.
So all I can think of is either:
Find a way to bind another property to the TextBox as well?
Find a way of using UpdateSourceTrigger=PropertyChanged on the TextBox, but prevent the DataGrid being updated.
Any ideas?
For the null value in the Observable List you have to update the list with a copy of it and an Dispatcher.Invoke otherwise the gui will crash or show null I had a similar problem on update a observable list in a thread.
So you should write your changes in a list like:
//Your Copy List
ObservableCollection<CategoryModel> _ReceivedCategories;
//Your Execute command for the Gui
public void onUpdateExecuted(object parameter){
Dispatcher.Invoke(new Action(() => ReceivedCategories = new ObservableCollection <CategoryModel> (_ReceivedCategories));
}
//Your Undo Command if the Inputs are not ok
public void onUndoExecuted(object parameter){
Dispatcher.Invoke(new Action(() => _ReceivedCategories = new ObservableCollection <CategoryModel> (ReceivedCategories));
}
//Your Command on every input which is set
public void onInputExecuted(object parameter){
_ReceivedCategories.add(Your Object);
}
And you can add an update Command for the Main List so you first update the Copy List with the values and after an Command set the Copy Collection in the Main Collection
Hope that helps
I couldn't get the above suggestion by SeeuD1 to work, but have have figured out what the problem was.
I have The DataGrid's ItemsSource bound to ReceivedCategories and in the code behind, the SelectionChanged event to use to bind to the TextBox.
After the database save, I call my GetCategories() again to refresh the data.
So when ReceivedCategories gets the data again, the DataGrid's ItemsSource is updated, but not before the SelectionChange event fires trying to set my CurrentCategory to the SelectedItem. However there is no longer anything selected in the DataGrid (index is -1), so assignment fails and sets CurrentCategory to null.
Fix is to simply only assign CurrentCategory to the SelectedItem if something is selected.
Old code:
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var viewmodel = (CategoriesViewModel)DataContext;
viewmodel.CurrentCategory = LstCategories.SelectedItems.Cast<CategoryModel>().FirstOrDefault();
}
Fixed:
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var viewmodel = (CategoriesViewModel)DataContext;
if (LstCategories.SelectedIndex > -1)
viewmodel.CurrentCategory = LstCategories.SelectedItems.Cast<CategoryModel>().FirstOrDefault();
}
Thanks for the suggestions.

SelectedItem doesn't bind in ListView in WPF

I've been trying to Bind the two ListViews to ViewModel. Both lists are loading the items properly. But to my surprise I've encountered a little problem.
The first ListView's SelectedItem binds correctly but the second one doesn't bind! As shown in the image below. What could be the reason?
XAML:
<Window x:Class="Test.Dialogs.BeamElevationsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Test.Dialogs.Converters"
Title="Select Beam Elevation" Height="350" Width="460"
Style="{StaticResource DialogStyle}"
WindowStartupLocation="CenterScreen">
<Window.Resources>
<converters:ElevationValueConverter x:Key="ElevationValueConverter"/>
</Window.Resources>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<GroupBox>
<Grid Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="175"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="215"/>
</Grid.ColumnDefinitions>
<GroupBox Header="Typs">
<ListView ItemsSource="{Binding TypIds}"
SelectedItem="{Binding CurrentTypId}">
<ListView.View>
<GridView AllowsColumnReorder="False"
ColumnHeaderContainerStyle="{StaticResource DialogsGridViewColumnHeaderStyle}" >
<GridViewColumn Header="Typ."/>
</GridView>
</ListView.View>
</ListView>
</GroupBox>
<GroupBox Grid.Row="0" Grid.Column="2" Header="Elevations">
<ListView ItemsSource="{Binding Elevations}"
SelectedItem="{Binding CurrentBeamElevation}">
<ListView.View>
<GridView AllowsColumnReorder="False"
ColumnHeaderContainerStyle="{StaticResource DialogsGridViewColumnHeaderStyle}" >
<GridViewColumn Header="Typ." />
</GridView>
</ListView.View>
</ListView>
</GroupBox>
</Grid>
</GroupBox>
<Grid Grid.Row="1">
<Button Content="OK"/>
</Grid>
</Grid>
</Window>
Code-Behind:
public partial class BeamElevationsWindow
{
private BeamElevationsViewModel ViewModel { get; set; }
public BeamElevationsWindow()
{
InitializeComponent();
ViewModel = new BeamElevationsViewModel();
DataContext = ViewModel;
}
}
ViewModel:
namespace Test.Dialogs.ViewModels
{
public class BeamElevationsViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public BeamElevationsViewModel()
{
var frames = Building.Frames
.GroupBy(f => f.TypId)
.Select(group => group.First())
.OrderBy(f => f.TypId)
.ToList();
typIds = new List<int>();
foreach (var frame in frames)
{
typIds.Add(frame.TypId);
}
TypIds = typIds;
CurrentTypId = Building.CurrentFrame.TypId;
GetElevations(CurrentTypId);
CurrentBeamElevation = Building.CurrentBeamElevation;
}
public void GetElevations(int typId)
{
var frames = Building.Frames
.Where(f => f.TypId == typId)
.OrderByDescending(f => f.Elevation)
.ToList();
elevations = new List<Elevation>();
foreach (var fr in frames)
{
foreach (var elevation in Building.Elevations)
{
if (Math.Abs(fr.Elevation - elevation.El) < Arithmetics.Tolerance)
{
elevations.Add(elevation);
break;
}
}
}
Elevations = elevations;
}
private List<int> typIds;
public List<int> TypIds
{
get { return typIds; }
private set
{
typIds = value;
RaisePropertyChanged("TypIds");
}
}
private int currentTypId;
public int CurrentTypId
{
get { return currentTypId; }
private set
{
currentTypId = value;
RaisePropertyChanged("CurrentTypId");
}
}
private List<Elevation> elevations;
public List<Elevation> Elevations
{
get { return elevations; }
private set
{
elevations = value;
RaisePropertyChanged("Elevations");
}
}
private Elevation currentBeamElevation;
public Elevation CurrentBeamElevation
{
get { return currentBeamElevation; }
private set
{
currentBeamElevation = value;
RaisePropertyChanged("CurrentBeamElevation");
}
}
private void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
The binding is actually working fine :)
However, the default comparer for object does a reference comparison. That means that when its trying to find the existing object in the list, it doesn't pick any of them because they aren't the same instance (per your comment).
The solution is to override Object.Equals (and when you override that, you should also override Object.GetHashCode). It should test equality based on some unique property of the object, so you don't get false positives.

Retrieving instance of class that was created dynamically

I have a button on my page so that when I click it another small window pops up letting you enter a Country. This will then appear as a label on my main screen.
This is is the save button.
<Button x:Name="btnSave" Content="Save" HorizontalAlignment="Left" Margin="31,82,0,0" VerticalAlignment="Top" Width="75" Click="btnSave_Click"/>
Then I have the code behind.
ICommand _AddCountry;
ViewModel _viewmodel;
public NewCountry(ICommand AddCountry, ViewModel viewModel)
{
InitializeComponent();
_Addcountry = AddCountry;
_viewmodel = viewModel;
}
private void btnSave_Click(object sender, RoutedEventArgs e)
{
string countryName = txtCountry.Text;
_viewmodel.RenameCountry(countryName);
_Addcountry.Execute(null);
this.Close();
}
The label is binded to the Title so that the Country will appear as that label.
<Label Content="{Binding Country}" Width="500" HorizontalAlignment="Left" >
Now this is my problem, when I create that Country at runtime, it creates a new instance of the NewCountryClass which has a <String>Country Property inside BUT also has a ICollection<string> Property named Places.
public class NewCountryClass : INotifyPropertyChanged
private string _country;
public string Country
{
get { return _country; }
set
{
if (_country.Equals(value))
return;
_country = value;
RaisePropertyChanged(() => Country);
}
}
private ICollection<string> _places;
public ICollection<string> Places
{
get
{
if (_places == null)
_places = new ObservableCollection<string>();
return _places;
}
set
{
if (value == _places)
return;
_places = value;
RaisePropertyChanged(() => Places);
}
}
When I right click on that Countrys label on my main screen and click Add Place on the drop down, I want that place to get added to that Country Instance of that class, inside the ICollection<string>Places.
The problem comes when if I create 3 titles on the screen(so it creates 3 different instances of AddCountryClass) that when I click Add place I cant find the instance of the Country I clicked on.
Other useful code in my ViewModel:
public void AddCountryCollection()
{
NewCountryClass newCountryClass = new NewCountyClass(newCountry,"");
Collections.Add(newCountryClass);
}
This is where I create the instance of the Country, When I enter my Country
public ObservableCollection<NewCountryClass> Collections
{
get { return collections; }
set { collections = value; OnPropertyChanged(() => Collections); }
}
This is where the Country gets passed**
public void AddPlaces()
{
NewCountryClass newCountryClass;
string Item = SelectedCountry.Country;
Collections.Select(w => w.Country);
}
This was me trying to have a go but It didn't achieve anything
Summary
If I create 3 Countrys which use 3 different instances of class NewContryClass, When I want to add something into the ICollection<string>Places of one of the Countrys, how do I find it?
disclaim i know non view stuff belongs in to the VM but thats the way i do it and i'm fine with it
MainV
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!--Here is how you could select your Country-->
<Label Content="Country" Grid.Column="0" Grid.Row="0" />
<ComboBox Grid.Column="1" Grid.Row="0"
DisplayMemberPath="Country" Margin="0,0,30,5"
ItemsSource="{Binding CountryCollection}"
SelectedItem="{Binding SelectedCountryItem,UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="+" Grid.Column="1" Grid.Row="0" Height="24" HorizontalAlignment="Right" Width="24" Margin="0,0,0,5"
Command="{Binding NewCountryCommand}"/>
<!--Here is how you could show your selected Country-->
<Label Content="Selected" Grid.Column="0" Grid.Row="1" />
<Grid Grid.Column="1" Grid.Row="1" DataContext="{Binding SelectedCountryItem}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Label Content="{Binding Country}"
Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2"/>
</Grid>
</Grid>
MainVM
private RelayCommand _NewCountryCommand; // you can find the RelayCommand Class on the web
public ICommand NewCountryCommand
{
get { return _NewCountryCommand ?? (_NewCountryCommand= new RelayCommand(param => this.OnNewCountry())); }
}
public void OnNewCountry()
{
var window = new Window(); // or whatever View you use
var vm = new NewCountryClass();
window.DataContext= vm;
window.ShowDialog();
if(vm.Result)// Result will be set to true
// if the user Click the Save Button in your CountryV
{
// here will be your logic if the user want to save his stuff
// in my case
CountryCollection.Add(vm)
SelectedCountryItem = vm;
}
}
public ObservableCollection<NewCountryClass> CountryCollection
{
get { return collections; }
set { collections = value;
OnPropertyChanged(() => CountryCollection); }
}
public NewCountryClass SelectedCountryItem
{
get { return selectedCountryItem; //}
set { selectedCountryItem = value;
OnPropertyChanged(() => SelectedCountryItem); }
}
and thats it.
You can also take a look at this Tut which provided some Samplecode

How to move items in a Grid

I've got a WPF Grid and would like to move rows up or down depending on the user's input. This is what I've tried so far (an example for when the user decides to move an element up):
RowDefinition currentRow = fieldsGrid.RowDefinitions[currentIndex];
fieldsGrid.RowDefinitions.Remove(currentRow);
fieldsGrid.RowDefinitions.Insert(currentIndex - 1, currentRow);
Am I doing something wrong? As the UI remains the same using this approach.
This would be the WPF approach to what you're screenshot looks like:
<Window x:Class="WpfApplication4.Window9"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window9" Height="300" Width="500">
<ItemsControl ItemsSource="{Binding Columns}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<DataTemplate.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisConverter"/>
</DataTemplate.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="50"/>
<ColumnDefinition/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="25"/>
<ColumnDefinition Width="25"/>
</Grid.ColumnDefinitions>
<!-- This is your Key image, I used a rectangle instead, you can change it -->
<Rectangle Fill="Yellow" Visibility="{Binding IsPrimaryKey, Converter={StaticResource BoolToVisConverter}}" Margin="2"/>
<CheckBox IsChecked="{Binding IsSelected}" Grid.Column="1"/>
<TextBlock Text="{Binding Name}" Grid.Column="2"/>
<ComboBox ItemsSource="{Binding SortOrders}" SelectedItem="{Binding SortOrder}" Grid.Column="3" Margin="2"/>
<Button Content="Up" Grid.Column="4" Margin="2"
Command="{Binding DataContext.MoveUpCommand, RelativeSource={RelativeSource FindAncestor, AncestorType=ItemsControl}}"
CommandParameter="{Binding}"/>
<Button Content="Down" Grid.Column="5" Margin="2"
Command="{Binding DataContext.MoveDownCommand, RelativeSource={RelativeSource FindAncestor, AncestorType=ItemsControl}}"
CommandParameter="{Binding}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
Code Behind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using InduraClientCommon.MVVM;
using System.Collections.ObjectModel;
namespace WpfApplication4
{
public partial class Window9 : Window
{
public Window9()
{
InitializeComponent();
var vm = new ColumnListViewModel();
vm.Columns.Add(new ColumnViewModel() { IsPrimaryKey = true, Name = "Customer ID", SortOrder = SortOrder.Ascending });
vm.Columns.Add(new ColumnViewModel() {Name = "Customer Name", SortOrder = SortOrder.Descending});
vm.Columns.Add(new ColumnViewModel() {Name = "Customer Age", SortOrder = SortOrder.Unsorted});
DataContext = vm;
}
}
}
ViewModel:
public class ColumnListViewModel: ViewModelBase
{
private ObservableCollection<ColumnViewModel> _columns;
public ObservableCollection<ColumnViewModel> Columns
{
get { return _columns ?? (_columns = new ObservableCollection<ColumnViewModel>()); }
}
private DelegateCommand<ColumnViewModel> _moveUpCommand;
public DelegateCommand<ColumnViewModel> MoveUpCommand
{
get { return _moveUpCommand ?? (_moveUpCommand = new DelegateCommand<ColumnViewModel>(MoveUp, x => Columns.IndexOf(x) > 0)); }
}
private DelegateCommand<ColumnViewModel> _moveDownCommand;
public DelegateCommand<ColumnViewModel> MoveDownCommand
{
get { return _moveDownCommand ?? (_moveDownCommand = new DelegateCommand<ColumnViewModel>(MoveDown, x => Columns.IndexOf(x) < Columns.Count)); }
}
private void MoveUp(ColumnViewModel item)
{
var index = Columns.IndexOf(item);
Columns.Move(index, index - 1);
MoveUpCommand.RaiseCanExecuteChanged();
MoveDownCommand.RaiseCanExecuteChanged();
}
private void MoveDown(ColumnViewModel item)
{
var index = Columns.IndexOf(item);
Columns.Move(index, index + 1);
MoveUpCommand.RaiseCanExecuteChanged();
MoveDownCommand.RaiseCanExecuteChanged();
}
}
public class ColumnViewModel: ViewModelBase
{
private bool _isPrimaryKey;
public bool IsPrimaryKey
{
get { return _isPrimaryKey; }
set
{
_isPrimaryKey = value;
NotifyPropertyChange(() => IsPrimaryKey);
}
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
NotifyPropertyChange(() => IsSelected);
}
}
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
NotifyPropertyChange(() => Name);
}
}
private List<SortOrder> _sortOrders;
public List<SortOrder> SortOrders
{
get { return _sortOrders ?? (_sortOrders = Enum.GetValues(typeof(SortOrder)).OfType<SortOrder>().ToList()); }
}
private SortOrder _sortOrder;
public SortOrder SortOrder
{
get { return _sortOrder; }
set
{
_sortOrder = value;
NotifyPropertyChange(() => SortOrder);
}
}
}
public enum SortOrder {Unsorted, Ascending, Descending}
}
This is what it looks like in my screen:
As you can see in the above example, I am in no way manipulating or creating UI elements in code, because it's actually not necessary. Whenever you need to interact with the pieces of information displayed in the screen, you interact with the ViewModels and not the View. This is the clear separation of concerns between UI and application logic WPF makes possible, which is utterly absent in other frameworks. Please consider this approach the de-facto default when doing any kind o N-element UIs in WPF.
Edit:
Advantages of this approach versus the classic one:
No need to manipulate complex WPF classes (I.E UI elements) in your code in order to show / get data from screen (just simple, simple
properties and INotifyPropertyChanged)
Scales better (UI can be anything as long as it honors the ViewModel properties, you could change the ComboBox to a rotating 3d
pink elephant with a Sort order in each foot.
No need to navigate the visual tree to find elements located God knows where.
No need to foreach anything. Just a simple Select that converts your data (from whatever data source you obtained it) to the
ViewModel list.
Bottom line: WPF is much simpler and nicer than anything else currently in existence, if you use the WPF approach.
Here is a quick example of using an ItemsControl to do what you are wanting:
ViewModel
public class ListBoxViewModel
{
private static readonly List<string> sortList = new List<string>() { "Unsorted", "Sorted" };
public List<string> SortList { get { return sortList; } }
public ObservableCollection<ItemDetail> ItemDetails { get; set; }
#region Up Command
ICommand upCommand;
public ICommand UpCommand
{
get
{
if (upCommand == null)
{
upCommand = new RelayCommand(UpExecute);
}
return upCommand;
}
}
private void UpExecute(object param)
{
var id = param as ItemDetail;
if (id != null)
{
var curIndex = ItemDetails.IndexOf(id);
if (curIndex > 0)
ItemDetails.Move(curIndex, curIndex - 1);
}
}
#endregion Up Command
#region Down Command
ICommand downCommand;
public ICommand DownCommand
{
get
{
if (downCommand == null)
{
downCommand = new RelayCommand(DownExecute);
}
return downCommand;
}
}
private void DownExecute(object param)
{
var id = param as ItemDetail;
if (id != null)
{
var curIndex = ItemDetails.IndexOf(id);
if (curIndex < ItemDetails.Count-1)
ItemDetails.Move(curIndex, curIndex + 1);
}
}
#endregion Down Command
public ListBoxViewModel()
{
ItemDetails = new ObservableCollection<ItemDetail>()
{
new ItemDetail() { IsSelected = false, ItemName = "Customer Id", SortOrder = "Unsorted" },
new ItemDetail() { IsSelected = true, ItemName = "Customer Name", SortOrder = "Sorted" },
new ItemDetail() { IsSelected = false, ItemName = "Customer Age", SortOrder = "Unsorted" }
};
}
}
ItemDetail Class (Made up by me to make things easier)
public class ItemDetail
{
public bool IsSelected { get; set; }
public string ItemName { get; set; }
public string SortOrder { get; set; }
}
XAML
<UserControl.Resources>
<DataTemplate DataType="{x:Type vm:ItemDetail}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="CheckBoxGroup" />
<ColumnDefinition SharedSizeGroup="ItemNameGroup" />
<ColumnDefinition SharedSizeGroup="SortGroup" />
<ColumnDefinition Width="20" />
<ColumnDefinition SharedSizeGroup="UpArrowGroup" />
<ColumnDefinition SharedSizeGroup="DownArrowGroup" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0" IsChecked="{Binding IsSelected}" VerticalAlignment="Center" />
<Label Grid.Column="1" Content="{Binding ItemName}" />
<ComboBox Grid.Column="2" ItemsSource="{Binding DataContext.SortList, RelativeSource={RelativeSource AncestorType={x:Type views:ListBoxExample}}}" SelectedItem="{Binding SortOrder}" />
<Button Grid.Column="4" Command="{Binding DataContext.UpCommand, RelativeSource={RelativeSource AncestorType={x:Type views:ListBoxExample}}}" CommandParameter="{Binding}">
<Image Source="..\images\up.png" Height="10" />
</Button>
<Button Grid.Column="5" Command="{Binding DataContext.DownCommand, RelativeSource={RelativeSource AncestorType={x:Type views:ListBoxExample}}}" CommandParameter="{Binding}">
<Image Source="..\images\down.png" Height="10" />
</Button>
</Grid>
</DataTemplate>
</UserControl.Resources>
<Grid Grid.IsSharedSizeScope="True">
<ItemsControl ItemsSource="{Binding ItemDetails}" />
</Grid>
And finally the results:
And after pressing the down arrow on the first item:
Hope this helps.
You are changing the order of the RowDefinitions, which is not what you want. You want to change the assignment of elements to rows, which is determined by the Grid.Row attached property
I would put all controls that belong to each row in a container (one per row) and then use Grid.SetRow to change the containers around. See how to change the grid row of the control from code behind in wpf.

Categories

Resources