Create a ListView With 3 Columns Xamarin Forms - c#

I want to create a customListView in Xamarin Forms that shows 3 columns and N rows (I will get the source from a web service). Each cell has an icon and a description. I have found several examples but no one uses columns.

Depending on what you want, you can create a ListView then have a Grid for each Item, where you define the 3 columns. This will allow you to keep the extra features of the ListView like PullToRefresh.
If you want an automatic GridView type control, I did build one off ChaseFlorell's example as shown in this forum post: https://forums.xamarin.com/discussion/61925/how-to-make-the-dynamic-grid-view-and-make-it-clickable
This way it makes it data bindable, rather than having to explicitly define each one.
First is the Grid Control
<Grid xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Mobile.Controls.GridView">
</Grid>
public partial class GridView : Grid
{
public GridView()
{
InitializeComponent();
for (var i = 0; i < MaxColumns; i++)
ColumnDefinitions.Add(new ColumnDefinition());
}
public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create<GridView, object>(p => p.CommandParameter, null);
public static readonly BindableProperty CommandProperty = BindableProperty.Create<GridView, ICommand>(p => p.Command, null);
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create<GridView, IEnumerable<object>>(p => p.ItemsSource, null, BindingMode.OneWay, null, (bindable, oldValue, newValue) => { ((GridView)bindable).BuildTiles(newValue); });
private int _maxColumns = 2;
private float _tileHeight = 0;
public Type ItemTemplate { get; set; } = typeof(DocumentTypeTemplate);
public int MaxColumns
{
get { return _maxColumns; }
set { _maxColumns = value; }
}
public float TileHeight
{
get { return _tileHeight; }
set { _tileHeight = value; }
}
public object CommandParameter
{
get { return GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public IEnumerable<object> ItemsSource
{
get { return (IEnumerable<object>)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public void BuildTiles(IEnumerable<object> tiles)
{
try
{
if (tiles == null || tiles.Count() == 0)
Children?.Clear();
// Wipe out the previous row definitions if they're there.
RowDefinitions?.Clear();
var enumerable = tiles as IList ?? tiles.ToList();
var numberOfRows = Math.Ceiling(enumerable.Count / (float)MaxColumns);
for (var i = 0; i < numberOfRows; i++)
RowDefinitions?.Add(new RowDefinition { Height = TileHeight });
for (var index = 0; index < enumerable.Count; index++)
{
var column = index % MaxColumns;
var row = (int)Math.Floor(index / (float)MaxColumns);
var tile = BuildTile(enumerable[index]);
Children?.Add(tile, column, row);
}
}
catch { // can throw exceptions if binding upon disposal
}
}
private Layout BuildTile(object item1)
{
var buildTile = (Layout)Activator.CreateInstance(ItemTemplate, item1);
buildTile.InputTransparent = false;
var tapGestureRecognizer = new TapGestureRecognizer
{
Command = Command,
CommandParameter = item1,
NumberOfTapsRequired = 1
};
buildTile?.GestureRecognizers.Add(tapGestureRecognizer);
return buildTile;
}
}
Then define a template
<Grid xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Mobile.TypeTemplate">
<Label Text="{Binding Name}" />
</Grid>
public partial class TypeTemplate : Grid
{
public TypeTemplate()
{
InitializeComponent();
}
public TypeTemplate(object item)
{
InitializeComponent();
BindingContext = item;
}
}
Then use the control
<control:GridView HorizontalOptions="FillAndExpand"
Grid.Row="1"
VerticalOptions="FillAndExpand"
RowSpacing="20"
ColumnSpacing="20"
MaxColumns="2"
ItemsSource="{Binding ListOfData}"
CommandParameter="{Binding}"
Command="{Binding ClickCommand}"
IsClippedToBounds="False">
<control:GridView.TileHeight>
<OnPlatform x:TypeArguments="x:Single"
iOS="60"
Android="60"
WinPhone="90" />
</control:GridView.TileHeight>
</control:GridView>

A much easier solution than the proposed is to use the DevExpress Grid control. It has support for headers, row virtualization, pull-to-refresh, load on demand and a bunch of other useful features.

Related

WPF MVVM Pagination

I have a user control for pagination view, it has its own viewmodel. I added the pagination in a page with a datagrid that also has a separate viewmodel.
My question is how can I update the ObservableCollection I have in my page viewmodel every time a command is done in my pagination viewmodel?
Here's my PagingControl.xaml
<StackPanel Width="Auto"
Orientation="Horizontal">
<Button
Margin="4,0"
Content="<<"
Command="{Binding FirstCommand}"/>
<Button
Margin="4,0"
Content="<"
Command="{Binding PreviousCommand}"/>
<StackPanel
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock
Text="{Binding Start}"/>
<TextBlock
Text=" to "/>
<TextBlock
Text="{Binding End}"/>
<TextBlock
Text=" of "/>
<TextBlock
Text="{Binding TotalItems}"/>
</StackPanel>
<Button
Margin="4,0"
Content=">"
Command="{Binding NextCommand}"/>
<Button
Margin="4,0"
Content=">>"
Command="{Binding LastCommand}"/>
<ComboBox Width="100" ItemsSource="{Binding ItemsPerPage}" SelectedValue="{Binding ItemCount}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding CountChangedCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
</StackPanel>
PagingViewModel.cs
public class PagingViewModel : ViewModelBase
{
private ObservableCollection<DataModel> _data;
private int start = 0;
private int itemCount = 10;
private int totalItems = 0;
private readonly List<int> count;
private ICommand _firstCommand;
private ICommand _previousCommand;
private ICommand _nextCommand;
private ICommand _lastCommand;
private ICommand _countchangedCommand;
public ObservableCollection<DataModel> Data
{
get { return _data; }
set
{
if (_data!= value)
{
_data= value;
OnPropertyChanged("Data");
}
}
}
public PagingViewModel()
{
count = new List<int> { 10, 20, 30};
RefreshData();
}
public int Start { get { return start + 1; } }
public int End { get { return start + itemCount < totalItems ? start + itemCount : totalItems; } }
public int TotalItems { get { return totalItems; } }
public List<int> Count { get { return count; } }
public int ItemCount { get { return itemCount; } set { itemCount = value; OnPropertyChanged("ItemCount"); } }
public ICommand FirstCommand
{
get
{
if (_firstCommand == null)
{
_firstCommand = new RelayCommand
(
param =>
{
start = 0;
RefreshData();
},
param =>
{
return start - itemCount >= 0 ? true : false;
}
);
}
return _firstCommand;
}
}
public ICommand PreviousCommand
{
get
{
if (_previousCommand == null)
{
_previousCommand = new RelayCommand
(
param =>
{
start -= itemCount;
RefreshData();
},
param =>
{
return start - itemCount >= 0 ? true : false;
}
);
}
return _previousCommand;
}
}
public ICommand NextCommand
{
get
{
if (_nextCommand == null)
{
_nextCommand = new RelayCommand
(
param =>
{
start += itemCount;
RefreshData();
},
param =>
{
return start + itemCount < totalItems ? true : false;
}
);
}
return _nextCommand;
}
}
public ICommand LastCommand
{
get
{
if (_lastCommand == null)
{
_lastCommand = new RelayCommand
(
param =>
{
start = (totalItems / itemCount - 1) * itemCount;
start += totalItems % itemCount == 0 ? 0 : itemCount;
RefreshData();
},
param =>
{
return start + itemCount < totalItems ? true : false;
}
);
}
return _lastCommand;
}
}
public ICommand CountChangedCommand
{
get
{
if (_countchangedCommand == null)
{
_countchangedCommand = new RelayCommand
(
param =>
{
start = 0;
RefreshData();
},
param =>
{
return ((totalItems - itemCount) > -10) ? true : false;
}
);
}
return _countchangedCommand;
}
}
public void RefreshData()
{
_data= GetData(start, itemCount, out totalItems);
DataViewModel vm = new DataViewModel(this);
OnPropertyChanged("Start");
OnPropertyChanged("End");
OnPropertyChanged("TotalItems");
}
}
And here's the viewmodel for my Page: DataViewModel.cs
public class DataViewModel: ViewModelBase
{
private ObservableCollection<DataModel> _data;
public ObservableCollection<DataModel> Data
{
get { return _data; }
set
{
if (_data!= value)
{
_data= value;
OnPropertyChanged("Data");
}
}
}
public DataViewModel(PagingViewModel pagevm)
{
_data = new ObservableCollection<DataModel>();
_data= pagevm.Data;
}
}
My Data property is bound to an ItemSource of a DataGrid in DataView.xaml with DataContext set to DataViewModel.
That's a nicely detailled question !
For your refresh problems i see a few options :
When setting your Data in RefreshData you should use the public setter of the property and not _data. If you don't you will never use the OnPropertyChanged to notify the view that your whole collection changed.
So you need to replace :
_data= GetData(start, itemCount, out totalItems);
With :
Data= GetData(start, itemCount, out totalItems);
By the way your DataViewModel makes no sense to me. Your _audits field is nowhere to be seen and the ObservableCollection Data is never set in this ViewModel. I belive your problem must come from this.
PS :
On the other hand i have some advices not directly related to your question:
First of all, when you want to to check if some RelayCommand or else is null before setting it you may want to use the ?? operator : https://msdn.microsoft.com/en-us/en-en/library/ms173224.aspx.
Secondly i highly recommand you to put your RelayCommands behaviours in methods. When you end up with dozen of commands it's a real mess to maintain RelayCommand where everything happens in lambdas.
This way you will replace this:
public ICommand NextCommand
{
get
{
if (_nextCommand == null)
{
_nextCommand = new RelayCommand
(
param =>
{
start += itemCount;
RefreshData();
},
param =>
{
return start + itemCount < totalItems ? true : false;
}
);
}
return _nextCommand;
}
}
With this :
public ICommand NextCommand
{
get
{
return _nextCommand = _nextCommand ?? new RelayCommand(Next, CanExecuteNext);
}
}
private void Next()
{
start += itemCount;
RefreshData();
}
private bool CanExecuteNext()
{
return start + itemCount < totalItems ? true : false;
}
It sounds like your commands need to be able to access your page view model. For them to do this your paging viewModel would need to hold a reference to the page viewModel (so that your paging viewModel could pass this to the relevant command).
I suggest using property injection. In your PagingViewModel add a property like this;
public AuditTrailViewModel AuditTrailViewModel { get; set; }
(Give the property change notification if you need to).
In your commands you would now be able to access the properties of the AuditTrailViewModel
public ICommand LastCommand
{
get
{
if (_lastCommand == null)
{
_lastCommand = new RelayCommand
(
param =>
{
start = (totalItems / itemCount - 1) * itemCount;
start += totalItems % itemCount == 0 ? 0 : itemCount;
AuditTrailViewModel.Data = //Now you can update your viewModel
RefreshData();
},
param =>
{
return start + itemCount < totalItems ? true : false;
}
);
}
return _lastCommand;
}
}

WPF ComboBox reloading and setting displayite which is an object an object

I have been working in a bespoke control based upon the WPF ComboBox
code below
enter code here<UserControl x:Class="wpfColorCombo.ColorPicker"
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="32" d:DesignWidth="190">
<UserControl.Resources>
<DataTemplate x:Key="LoadedValue">
<StackPanel Orientation="Horizontal">
<TextBlock Width="10"
Height="10"
Margin="5"
Background="Aqua"/>
<TextBlock Text="Aqua"
Margin="5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<ComboBox x:Name="colorPickerCombo"
ItemsSource="{Binding Path=FontColors}"
DropDownClosed="colourPickerCombo_DropDownClosed"
SelectedValue="{Binding Path=SelectedFontColor, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
IsSynchronizedWithCurrentItem="True"
Loaded="colourPickerCombo_Loaded">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Width="10"
Height="10"
Margin="5"
Background="{Binding Name}"/>
<TextBlock Text="{Binding Name}"
Margin="5"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
The controls code behind:
public partial class ColorPicker : UserControl
{
public ColorPicker()
{
InitializeComponent();
DataContext = new viewModelColorPicker();
}
}
Nothing unusual here except when I come to reload the control with with saved data. I seem to be unable to find a means to get the displayed. I have looked for the last half day for a solution and the nearest I have come is based upon a similar problem in a Grid control. The solution there was to assign the windows resource in this case LoadedValue to the content. In a ComboBox I have Text. I have tried to utilize TextBlock but without a stack panel to aid lay outs I am stuck.
Any help that may get me closer is good :)
Cheers Angry Bobb
I've seen issues like this, mostly these problem occurs when you overwrite the bounded instance.
Like, when you load the data, (for example with xmlserializer) a new instance of the FontColors will overwrite the old one. The combobox is still bounded to the old instance, so the new one doesn't show up.
So, you probably need to reassign/bind the ItemsSource of the Combobox after loading.
This is the code behind
public class viewModelColorPicker : ViewModelBase
{
modelFont colours = new modelFont();
public viewModelColorPicker()
{
}
private ObservableCollection<FontColor> fontColours;
public ObservableCollection<FontColor> FontColours
{
get
{
fontColours = colours.AvalibleColours;
return fontColours;
}
}
private FontColor selectedColour = new FontColor("Black",Brushes.Black);
public FontColor SelectedColour
{
get
{
selectedColour = colours.SelectedColor.Coalesce(new FontColor("Black",Brushes.Black));
return selectedColour;
}
set
{
SetProperty(ref selectedColour, value, "SelectedColour");
colours.SelectedColor = value;
}
}
}
These three classes are the model
public class modelFont : ViewModelBase
{
ObservableCollection<FontColor> availableColours = new ObservableCollection<FontColor>();
public modelFont()
{
AvailableColors AvCol = new AvailableColors();
availableColours = AvCol.GetFontColorsList().ToObservableCollection();
}
public ObservableCollection<FontColor> AvalibleColours
{
get { return availableColours; }
set { SetProperty(ref this.availableColours, value, "AvalibleColours"); }
}
private FontColor selectedColor;
public FontColor SelectedColor
{
get { return selectedColor; }
set { SetProperty(ref this.selectedColor, value, "SelectedColor"); }
}
}
public class FontColor
{
public string Name { get; set; }
public SolidColorBrush Brush { get; set; }
public FontColor(string name, SolidColorBrush brush)
{
Name = name;
Brush = brush;
}
public override bool Equals(System.Object obj)
{
if (obj == null)
{
return false;
}
FontColor p = obj as FontColor;
if ((System.Object)p == null)
{
return false;
}
return (this.Name == p.Name) && (this.Brush.Equals(p.Brush));
}
public bool Equals(FontColor p)
{
if ((object)p == null)
{
return false;
}
return (this.Name == p.Name) && (this.Brush.Equals(p.Brush));
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public override string ToString()
{
return "FontColor [Color=" + this.Name + ", " + this.Brush.ToString() + "]";
}
}
}
class AvailableColors : List<FontColor>
{
#region Conversion Utils Static Methods
public static FontColor GetFontColor(SolidColorBrush b)
{
AvailableColors brushList = new AvailableColors();
return brushList.GetFontColorByBrush(b);
}
public static FontColor GetFontColor(string name)
{
AvailableColors brushList = new AvailableColors();
return brushList.GetFontColorByName(name);
}
public static FontColor GetFontColor(Color c)
{
return AvailableColors.GetFontColor(new SolidColorBrush(c));
}
public static int GetFontColorIndex(FontColor c)
{
AvailableColors brushList = new AvailableColors();
int idx = 0;
SolidColorBrush colorBrush = c.Brush;
foreach (FontColor brush in brushList)
{
if (brush.Brush.Color.Equals(colorBrush.Color))
{
break;
}
idx++;
}
return idx;
}
#endregion
public AvailableColors()
: base()
{
this.Init();
}
public FontColor GetFontColorByName(string name)
{
FontColor found = null;
foreach (FontColor b in this)
{
if (b.Name == name)
{
found = b;
break;
}
}
return found;
}
public FontColor GetFontColorByBrush(SolidColorBrush b)
{
FontColor found = null;
foreach (FontColor brush in this)
{
if (brush.Brush.Color.Equals(b.Color))
{
found = brush;
break;
}
}
return found;
}
private void Init()
{
Type brushesType = typeof(Colors);
var properties = brushesType.GetProperties(BindingFlags.Static | BindingFlags.Public);
foreach (var prop in properties)
{
string name = prop.Name;
SolidColorBrush brush = new SolidColorBrush((Color)(prop.GetValue(null, null)));
this.Add(new FontColor(name, brush));
}
}
}
These are the helper classes
public static class Ext_CollectionExtensions
{
public static ObservableCollection<DataRow> ToObservableCollection(this DataTable coll)
{
var c = new ObservableCollection<DataRow>();
foreach (DataRow e in coll.Rows)
c.Add(e);
return c;
}
public static ObservableCollection<T> ToObservableCollection<T>(this List<T> ListOfT)
{
ObservableCollection<T> returned = new ObservableCollection<T>();
foreach (var itm in ListOfT)
{
returned.Add(itm);
}
return returned;
}
}
public static class Ext_Object
{
public static T Coalesce<T>(this T obj, params T[] args)
{
if (obj != null || Convert.IsDBNull(obj))
return obj;
foreach (T arg in args)
{
if (arg != null || Convert.IsDBNull(obj))
return arg;
}
return default(T);
}
}
I have modified the code to be more concise. I did not write the original which I did not feel help in the solving of this problem because it was overly complex and verbose.
The answer after much fartting about is
this.SelectedColor = this.FontColors.Where(a => a.Name == "Black").FirstOrDefault();
All because the selection is the selection of a value in the supplying collection and not setting a value to display.

Databinding between two custom UserControls

I have two custom NumberUpDown user controls that need to interact with each other. The maximum value of the one on the left needs to be the minimum of the one on the right. To accomplish this I put them in another user control which contains both and has all the required data members.
The xaml the container user control looks like
<controls:NumberBox x:Name="LeftNumberBox" HorizontalAlignment="Right" Width="50" Min="{Binding Min}" Current="{Binding Left}" Max="{Binding Right}"/>
<controls:NumberBox x:Name="RightNumberBox" Grid.Column="2" HorizontalAlignment="Left" Width="50" Min="{Binding Left}" Current="{Binding Right}" Max="{Binding Max}"/>
and its cs file looks like
public ThresholdBox()
{
InitializeComponent();
}
public int Min
{
get
{
return (int)GetValue(MinIntProperty);
}
set
{
SetValue(MinIntProperty, value);
}
}
public static readonly DependencyProperty MinIntProperty =
DependencyProperty.Register("Min", typeof(int), typeof(ThresholdBox), new PropertyMetadata(0));
public int Max
{
get
{
return (int)GetValue(MaxIntProperty);
}
set
{
SetValue(MaxIntProperty, value);
}
}
public static readonly DependencyProperty MaxIntProperty =
DependencyProperty.Register("Max", typeof(int), typeof(ThresholdBox), new PropertyMetadata(100));
public int Left
{
get
{
return LeftNumberBox.Current;
}
set
{
LeftNumberBox.Current = value;
}
}
public int Right
{
get
{
if(RightNumberBox != null)
{
return RightNumberBox.Current;
}
else
{
return 10;
}
}
set
{
RightNumberBox.Current = value;
}
}
and the NumberBox aka NumberUpDown user control cs file looks like
public NumberBox()
{
InitializeComponent();
NUDTextBox.Text = Current.ToString();
Min = 0;
Current = 10;
Max = 100;
}
public int Min
{
get
{
return (int)GetValue(MinIntProperty);
}
set
{
SetValue(MinIntProperty, value);
}
}
public static readonly DependencyProperty MinIntProperty =
DependencyProperty.Register("Min", typeof(int), typeof(NumberBox), new PropertyMetadata(0));
public int Current
{
get
{
return (int)GetValue(CurrentIntProperty);
}
set
{
SetValue(CurrentIntProperty, value);
}
}
public static readonly DependencyProperty CurrentIntProperty =
DependencyProperty.Register("Current", typeof(int), typeof(NumberBox), new PropertyMetadata(10));
public int Max
{
get
{
return (int)GetValue(MaxIntProperty);
}
set
{
SetValue(MaxIntProperty, value);
}
}
public static readonly DependencyProperty MaxIntProperty =
DependencyProperty.Register("Max", typeof(int), typeof(NumberBox), new PropertyMetadata(100));
I didn't include the button logic because it works as expected
The container is called with
<controls:ThresholdBox Left="10" Right="90" Min="0" Max="100" Padding="0,20,0,20"/>
Unfortunately the code doesn't work as expected. Both NumberBoxes start at 10, the left one will go down to 0 and up to 10, while the right one goes up to 100 and no lower than 10, even if I lower the left NumberBoxes value to 0 in the UI.
I thought this might be a two way binding problem but that causes as StackOverFlow Exception. What am I missing in my binding code to make both NumberBoxes use/modify the container's Left and Right properties respectively?
If "The maximum value of the one on the left needs to be the minimum of the one on the right", why don't those two properties bind to the same value?
Add a new property to your ViewModel:
public int Intermediate { get; set; }
Then bind left Max and right Min to this property.
<controls:NumberBox x:Name="LeftNumberBox" HorizontalAlignment="Right" Width="50" Min="{Binding Min}" Current="{Binding Left}" Max="{Binding Intermediate}"/>
<controls:NumberBox x:Name="RightNumberBox" Grid.Column="2" HorizontalAlignment="Left" Width="50" Min="{Binding Intermediate}" Current="{Binding Right}" Max="{Binding Max}"/>

creating class structure in mvvm issues

I have the following Classes:
Item
public class Item : INotifyPropertyChanged, IDataErrorInfo
{
private int? id;
public int? ID
{
get
{ return id; }
set
{ id = value; }
}
private string name;
public string Name
{
get
{ return name; }
set
{
if (value != name)
{
ClearError("Name");
if (string.IsNullOrEmpty(value) || value.Trim() == "")
SetError("Name", "Required Value");
name = value;
}
}
}
private List<MedicineComposition> medicineCompositions;
public List<MedicineComposition> MedicineCompositions
{
set { medicineCompositions = value; }
get { return medicineCompositions; }
}
}
MedicineComposition
public class MedicineComposition : INotifyPropertyChanged, IDataErrorInfo
{
private int? id;
public int? ID
{
get
{ return id; }
set
{ id = value; }
}
private Item item;
public Item Item
{
get
{ return item; }
set
{
if (item != value)
{
ClearError("Item");
if (value == null)
SetError("Item", "Required Value");
item = value;
}
}
}
private Component component;
public Component Component
{
get
{ return component; }
set
{
if (component != value)
{
ClearError("Component");
if (value == null)
SetError("Component", "Required Value");
component = value;
}
}
}
}
Component which has only id and Name
and the following functions that bring data from database and make the list of my objects:
GetItems in Item Class
public static List<Item> GetAllItems
{
get
{
List<Item> MyItems = new List<Item>();
SqlConnection con = new SqlConnection(BaseDataBase.ConnectionString);
SqlCommand com = new SqlCommand("sp_Get_All_Item", con);
com.CommandType = System.Data.CommandType.StoredProcedure;
try
{
con.Open();
SqlDataReader rd = com.ExecuteReader();
while (rd.Read())
{
Item i = new Item();
if (!(rd["ID"] is DBNull))
i.ID = System.Int32.Parse(rd["ID"].ToString());
i.Name = rd["Name"].ToString();
i.MedicineCompositions = MedicineComposition.GetAllByItem(i);
MyItems.Add(i);
}
rd.Close();
}
catch
{
MyItems = null;
}
finally
{
con.Close();
}
return MyItems;
}
GetAllByItem in MedicalCompositions
public static List<MedicineComposition> GetAllByItem(Item i)
{
List<MedicineComposition> MyMedicineCompositions = new List<MedicineComposition>();
SqlConnection con = new SqlConnection(BaseDataBase.ConnectionString);
SqlCommand com = new SqlCommand("sp_Get_ByItemID_MedicineComposition", con);
com.CommandType = System.Data.CommandType.StoredProcedure;
SqlParameter pr = new SqlParameter("#ID", i.ID);
com.Parameters.Add(pr);
try
{
con.Open();
SqlDataReader rd = com.ExecuteReader();
while (rd.Read())
{
MedicineComposition m = new MedicineComposition() { };
if (!(rd["ID"] is DBNull))
m.ID = Int32.Parse(rd["ID"].ToString());
if (!(rd["ComponentID"] is DBNull))
m.Component = Component.GetByID(Int32.Parse(rd["ComponentID"].ToString()));
m.Item = i;
MyMedicineCompositions.Add(m);
}
rd.Close();
}
catch
{
MyMedicineCompositions = null;
}
finally
{
con.Close();
}
return MyMedicineCompositions;
}
it's like to use mvvm because it let you deal with objects instead of datatable, but when i use the previous shape of class structure i have the following problems:
i have at least 1000 records in Item Table in database so when i call GetAllItems i have slow performance especially when the database in not on local computer.
i tried to load Items when splash screen on, it takes times but take medium performance
on each update on Item table i should recall GetAllItems so slow back
my questions is where is the problem that i have in creating class, and is this the best way to structure the class in mvvm
I dont think your user need to see all the 1000 items at a glance, not even the many thousand of composition and components related.
I situations like this I would:
Filter the data. Ask the user for the Item name, category or what else.
Delay load. At first load only the (filtered) Items. When the user select an Item switch to an "Item details" View and load the related data (composition and components).
A few things you could improve here, for example:
Given we are talking about MedicalComposition it might not be best of ideas to have nullable unique identifier
If you have multiple classes consisting only of id and name you could use KeyValuePair<> or Tuple<> instead
Implement a base class, say ModelBase that implements INotifyPropertyChanged
Implement repository pattern for database related action, cache/page results if possible
If not yet done, move data- and/or time intensive operations into separate thread(s)
It's a little confusing that on Item you have IEnumerable of MedicineCompositions but also, in MedicineComposition you have the Item, too? Maybe you don't need it at all or related Item.Id would be enough?
You could add a method to your repository to only return items that have been added/modified/removed since <timestamp> and only update what is necessary in your Items collection
You could make some of the properties Lazy<>
Utilize TAP (Task-based Asynchronous Pattern)
Below is "one go" for your problem w/o blocking the UI thread. It's far from complete but still. Thread.Sleeps in repositories are mimicking your database query delays
View\MainWindow.xaml
Codebehind contains just InitializeComponents.
<Window x:Class="WpfApplication1.View.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModel="clr-namespace:WpfApplication1.ViewModel"
Title="MainWindow"
Height="300"
Width="250">
<Window.DataContext>
<viewModel:MainViewModel />
</Window.DataContext>
<!-- Layout root -->
<Grid x:Name="ContentPanel" Margin="12,0,12,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Status label -->
<Label Grid.Row="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Background="Bisque"
Margin="0,3,0,3"
Content="{Binding Status}" />
<!-- Controls -->
<StackPanel Grid.Row="1">
<Label Content="Items" />
<!-- Items combo -->
<ComboBox HorizontalAlignment="Stretch"
MaxDropDownHeight="120"
VerticalAlignment="Top"
Width="Auto"
Margin="0,0,0,5"
ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem}"
DisplayMemberPath="Name" />
<!-- Medicine components -->
<ItemsControl ItemsSource="{Binding SelectedItem.MedicineCompositions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}" />
<!-- Components -->
<ItemsControl ItemsSource="{Binding Components}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run Text=" * " />
<Run Text="{Binding Name}" />
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</Window>
ViewModel\MainViewModel
public class MainViewModel : ViewModelBase
{
private string _status;
private Item _selectedItem;
private ObservableCollection<Item> _items;
public MainViewModel()
:this(new ItemRepository(), new MedicineCompositionRepository())
{}
public MainViewModel(IRepository<Item> itemRepository, IRepository<MedicineComposition> medicineCompositionRepository)
{
ItemRepository = itemRepository;
MedicineCompositionRepository = medicineCompositionRepository;
Task.Run(() => LoadItemsData());
}
public IRepository<Item> ItemRepository { get; set; }
public IRepository<MedicineComposition> MedicineCompositionRepository { get; set; }
public Item SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
OnPropertyChanged();
Task.Run(() => LoadMedicineCompositionsData(_selectedItem));
}
}
public ObservableCollection<Item> Items
{
get { return _items; }
set { _items = value; OnPropertyChanged(); }
}
public string Status
{
get { return _status; }
set { _status = value; OnPropertyChanged(); }
}
private async Task LoadItemsData()
{
Status = "Loading items...";
var result = await ItemRepository.GetAll();
Items = new ObservableCollection<Item>(result);
Status = "Idle";
}
private async Task LoadMedicineCompositionsData(Item item)
{
if (item.MedicineCompositions != null)
return;
Status = string.Format("Loading compositions for {0}...", item.Name);
var result = await MedicineCompositionRepository.GetById(item.Id);
SelectedItem.MedicineCompositions = result;
Status = "Idle";
}
}
Model
public class Component : ModelBase
{}
public class MedicineComposition : ModelBase
{
private IEnumerable<Component> _component;
public IEnumerable<Component> Components
{
get { return _component; }
set { _component = value; OnPropertyChanged(); }
}
}
public class Item : ModelBase
{
private IEnumerable<MedicineComposition> _medicineCompositions;
public IEnumerable<MedicineComposition> MedicineCompositions
{
get { return _medicineCompositions; }
set { _medicineCompositions = value; OnPropertyChanged(); }
}
}
public abstract class ModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int _id;
private string _name;
public int Id
{
get { return _id; }
set { _id = value; OnPropertyChanged(); }
}
public string Name
{
get { return _name; }
set { _name = value; OnPropertyChanged(); }
}
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Repository
public interface IRepository<T> where T : class
{
Task<IEnumerable<T>> GetAll();
Task<IEnumerable<T>> GetById(int id);
}
public class ItemRepository : IRepository<Item>
{
private readonly IList<Item> _mockItems;
public ItemRepository()
{
_mockItems = new List<Item>();
for (int i = 0; i < 100; i++)
_mockItems.Add(new Item { Id = i, Name = string.Format("Item #{0}", i), MedicineCompositions = null });
}
public Task<IEnumerable<Item>> GetAll()
{
Thread.Sleep(1500);
return Task.FromResult((IEnumerable<Item>) _mockItems);
}
public Task<IEnumerable<Item>> GetById(int id)
{
throw new NotImplementedException();
}
}
public class MedicineCompositionRepository : IRepository<MedicineComposition>
{
private readonly Random _random;
public MedicineCompositionRepository()
{
_random = new Random();
}
public Task<IEnumerable<MedicineComposition>> GetAll()
{
throw new NotImplementedException();
}
public Task<IEnumerable<MedicineComposition>> GetById(int id)
{
// since we are mocking, id is actually ignored
var compositions = new List<MedicineComposition>();
int compositionsCount = _random.Next(1, 3);
for (int i = 0; i <= compositionsCount; i++)
{
var components = new List<Component>();
int componentsCount = _random.Next(1, 3);
for (int j = 0; j <= componentsCount; j++)
components.Add(new Component {Id = j, Name = string.Format("Component #1{0}", j)});
compositions.Add(new MedicineComposition { Id = i, Name = string.Format("MedicalComposition #{0}", i), Components = components });
}
Thread.Sleep(500);
return Task.FromResult((IEnumerable<MedicineComposition>) compositions);
}
}
Instead of returning List, return IEnumerable and yield results as they are needed. Obviously it would only improve performance, when you are not reading all the results, which is actually true in most cases. To do that you would have to remove catch, because you cannot have yield and catch together. The catch could go around con.Open and ExecuteReader and in catch you can yield break:
public static IEnumerable<MedicineComposition> GetAllByItem(Item i)
{
SqlConnection con = new SqlConnection(BaseDataBase.ConnectionString);
SqlCommand com = new SqlCommand("sp_Get_ByItemID_MedicineComposition", con);
com.CommandType = System.Data.CommandType.StoredProcedure;
SqlParameter pr = new SqlParameter("#ID", i.ID);
com.Parameters.Add(pr);
try
{
SqlDataReader rd;
try
{
con.Open();
rd = com.ExecuteReader();
}
catch { yield break;}
while (rd.Read())
{
MedicineComposition m = new MedicineComposition() { };
if (!(rd["ID"] is DBNull))
m.ID = Int32.Parse(rd["ID"].ToString());
if (!(rd["ComponentID"] is DBNull))
m.Component = Component.GetByID(Int32.Parse(rd["ComponentID"].ToString()));
m.Item = i;
yield return m;
}
rd.Close();
}
finally
{
con.Close();
}
}
Now in case of an exception this is no longer returning null, but can return few items or even empty enumeration. I would rather move the catch to caller of this getter.
If you need for some reason count of returned items, call GetAllByItem(item).ToArray(). This will enumerate all the items once and gets the length for you. Definitely don't call the enumeration twice to get the length and then enumerate the items:
var length = GetAllByItem(item).Count();// this will get all the items from the db
foreach(var i in GetAllByItem(item)) // this will get all the items from the db again
Rather do this:
var list = GetAllByItem(item); // this will get all the items and now you have the length and the items.
Obviously if you need the length for some reason, there is no point in changing to IEnumerable, only for better abstraction.
Other improvement could be, to create the connection only once instead of every time on calling the getter. That is possible only, if you know it won't cause any harm.
Assign the dataset into the constructor of your ObservableCollection property. Else your view will update via a PropertyChanged notification for each item that your ObservableCollection performs an Add operation.
Try this:
var items = services.LoadItems();
myObservableCollection = new ObservableCollection<somedatatype>(items);
This type of assignment will notify your view once instead of the current way your implementation does which is 1000 times.

Problems binding a DataGrid with an Observable Collection that receive null parameters in WPF

I'm new programming with C# in WPF, and I have an error:
I've bind my WPF DataGRid with an Observable Collection that receive parameters from a DataBase. First, when the Observable Collection receive all the parameters from the Stored Procedure the APP function normally, but when I have a null parameter into my data source (StoredProcedure), so the Observable Collection receive a null parameter, the application crash, and it don´t start.... the page just remain in blank
How to fix that problem ?
First Here I show the class I used as a Dada Context to my Page
class ColeccionDeDatos : INotifyPropertyChanged
{
private RegistroBLL regBll = new RegistroBLL();
private DivisionBLL divBll = new DivisionBLL();
private BrigadaBLL briBll = new BrigadaBLL();
private BatallonBLL batBll = new BatallonBLL();
private TropasBLL tropBll = new TropasBLL();
private CompañiaBLL compBLL = new CompañiaBLL();
private EstudioBLL estBll = new EstudioBLL();
private RegistroFullBLL regFullBll = new RegistroFullBLL();
private ObservableCollection<RegistroFullBO> coleccionFullRegistro = new ObservableCollection<RegistroFullBO>();
public ObservableCollection<RegistroFullBO> ColeccionFullRegistro
{
get { return coleccionFullRegistro; }
set { coleccionFullRegistro = value; }
}
private ObservableCollection<RegistroBO> coleccionRegistro = new ObservableCollection<RegistroBO>();
public ObservableCollection<RegistroBO> ColeccionRegistro
{
get { return coleccionRegistro; }
set { coleccionRegistro = value; }
}
private ObservableCollection<DivisionBO> coleccionDivision = new ObservableCollection<DivisionBO>();
public ObservableCollection<DivisionBO> ColeccionDivision
{
get { return coleccionDivision; }
set { coleccionDivision = value; }
}
private ObservableCollection<BrigadaBO> coleccionBrigada = new ObservableCollection<BrigadaBO>();
public ObservableCollection<BrigadaBO> ColeccionBrigada
{
get { return coleccionBrigada; }
set { coleccionBrigada = value; }
}
private ObservableCollection<BatallonBO> coleccionBatallon = new ObservableCollection<BatallonBO>();
public ObservableCollection<BatallonBO> ColeccionBatallon
{
get { return coleccionBatallon; }
set { coleccionBatallon = value; }
}
private ObservableCollection<TropasBO> coleccionTropas = new ObservableCollection<TropasBO>();
public ObservableCollection<TropasBO> ColeccionTropas
{
get { return coleccionTropas; }
set { coleccionTropas = value; }
}
private ObservableCollection<CompañiaBO> coleccionCompañia = new ObservableCollection<CompañiaBO>();
public ObservableCollection<CompañiaBO> ColeccionCompañia
{
get { return coleccionCompañia; }
set { coleccionCompañia = value; }
}
private ObservableCollection<EstudioBO> coleccionEstudio = new ObservableCollection<EstudioBO>();
public ObservableCollection<EstudioBO> ColeccionEstudio
{
get { return coleccionEstudio; }
set { coleccionEstudio = value; }
}
public ColeccionDeDatos()
{
ColeccionRegistro = regBll.ObtenerFilasRegistro();
ColeccionDivision = divBll.ObtenerFilasDivision();
ColeccionBrigada = briBll.ObtenerFilasBrigada();
ColeccionBatallon = batBll.ObtenerFilasBatallon();
ColeccionTropas = tropBll.ObtenerFilasTropas();
ColeccionCompañia = compBLL.ObtenerFilasCompañia();
ColeccionEstudio = estBll.ObtenerFilasEstudio();
ColeccionFullRegistro = regFullBll.ObtenerFilasRegistro();
}
Then In the code behind of the page a asign an instance of this class as DataContext for the Page element.
private void Page_Loaded(object sender, RoutedEventArgs e)
{
try
{
PagRegistroName.DataContext = colData;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message + "+++++++++++");
}
}
And for my Data Grid this one:
<DataGrid Name="dgRegistro" Margin="5" SelectionChanged="dgRegistro_SelectionChanged"
ItemsSource="{Binding Path=ColeccionFullRegistro}" AutoGenerateColumns="False">
In each cell template I used a combobox:
<DataGridTemplateColumn.CellEditingTemplate >
<DataTemplate>
<ComboBox x:Name="cmbDivision" Text="{Binding Path=Division, Mode=TwoWay}"
ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Page, AncestorLevel=1},
Path=DataContext.ColeccionDivision}" DisplayMemberPath="Nom_division" SelectionChanged="cmbDivision_SelectionChanged" SelectedValuePath="Nom_division">
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
so it is function a well as I want
https://onedrive.live.com/redir?resid=B371651233630F9A!153&authkey=!ADzRW6OvvdMZiMI&v=3&ithint=photo%2c.png
which corresponds to the table in DataBase
BUT, when I put a NULL value directly in the DataBase
http://1drv.ms/RrN5qO
The App start but still in blank everytime, so something is wrong..
Any idea what is the problem ?
Filter like this:
if(MyProperty == null)
{
MyProperty = string.Empty;
]

Categories