How refresh items on a content page? - c#

I read a lot of topics about usage of ObservableCollection, but I don't understand what should I do in my case. I have the following PageContent structure:
I use IList collection to store my Xamarin.Form.Image. I take it from the database. The number of the grid layout rows and columns definitely depend on the number of images in a database. User can load new images to a database. It ads to a database by using ImageRepository and it ads to the IList<T> collection, but I don't know how to dynamically redraw my grid layout. I tried to do this as follows:
//this.images
//is a IList<Images> which stores as property of the class.
private void OnClickSearchImagesButton(object sender, EventArgs args)
{
IFiler filer = DependencyService.Get<IFiler>();
// Get all paths of the images
IEnumerable<string> filesPath = filer.GetFilesPaths(FileExtension.JPG);
IEnumerable<Image> images = filesPath.Select(f => new Image { Source = ImageSource.FromFile(f) });
foreach (Image img in images)
{
// If found image does not exist
// Add it to a database
// --------------------
// Redraw grid layout
this.gridLayout = new Grid();
const int numberOfImagesPerRow = 3;
int numberOfImages = this.images.Count;
int numberOfRows = (int)Math.Ceiling((double)numberOfImages / numberOfImagesPerRow);
// configure grid layout
for (int i = 0; i < numberOfRows; i++)
{
this.gridLayout.RowDefinitions.Add(new RowDefinition { Height = new GridLength(200) });
}
for (int i = 0; i < numberOfImagesPerRow; i++)
{
this.gridLayout.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
}
for (int i = 0, imageNumber = 0; i < numberOfRows; i++)
{
for (int j = 0; j < numberOfImagesPerRow && imageNumber < numberOfImages; j++, imageNumber++)
{
this.gridLayout.Children.Add(images[imageNumber], j, i);
}
}
}
}
But it didn't help me. I saw some topics when people use ObservableCollection for items instead of List. I'm a little bit confused with it, because I think that I need to store all my items in ObservableCollection. Please, could you explain me what is the best way to hold actual information on the page?

ObservableCollection is very useful when you use controls like ListView. If you add a record or remove a record from the ObservableCollection, ListView updates automatically.
If you create a Grid and add Images to it, you should add or remove items "by code". ObservableCollection can't help you.
If you need a ListView that display images (so you can use ObservableCollection) you can take a look to
FlowListView
it's a powerful plugin that can visualize Images in a List
DataTemplate and DataTemplateSelector support
Fixed or automatic column count
Grouping support
Columns can expand to empty space (configurable)
ANY View can be used as a cell
All Xamarin.Forms platforms supported
Simple sample
<flv:FlowListView FlowColumnCount="3" SeparatorVisibility="None" HasUnevenRows="false"
FlowItemTappedCommand="{Binding ItemTappedCommand}" FlowLastTappedItem="{Binding LastTappedItem}"
FlowItemsSource="{Binding Items}" >
<flv:FlowListView.FlowColumnTemplate>
<DataTemplate>
<Label HorizontalOptions="Fill" VerticalOptions="Fill"
XAlign="Center" YAlign="Center" Text="{Binding Title}"/>
</DataTemplate>
</flv:FlowListView.FlowColumnTemplate>
</flv:FlowListView>
If you need a demo
DEMO

ObservableCollection is helpful because it fires events when the collection changes, but you need to either use a control that consumes those events, or create one yourself. In your case, I would suggest looking at some third party controls that layout images as you want (another answer mentioned FlowListView), or look at creating a custom layout in Xamarin Forms.

Related

Accessing Grid.Children.Add in MVVM (WPF)

So I have this idea of adding several buttons to a grid, then arranging them, and the number of buttons added is related to a number I enter. All works fine, until I try to make it work under MVVM.
Code that works in the MainWindow.xaml.cs:
private void Button_Click(object sender, RoutedEventArgs e)
{
int i = 0, j = 0;
Brush[] bs = { Brushes.BurlyWood, Brushes.Honeydew };
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
Button btn = new Button();
btn.Height = 50;
btn.Width=50;
btn.SetValue(Grid.RowProperty, i);
btn.SetValue(Grid.ColumnProperty, j);
if ((i + j) % 2 == 0)
{
btn.Background=bs[0];
}
else btn.Background = bs[1];
GameGrid.Children.Add(btn);
}
}
}
So basically, I press a button and it adds 3x3 buttons, colored nicely and spread appropriately. But then I get to the MVVM:
private void ButtonClickCommand()
{
RowCount = GridNumber;
ColumnCount=GridNumber;
int i = 0, j = 0;
Brush[] bs = { Brushes.BurlyWood, Brushes.Honeydew };
for (i = 0; i < GridNumber; i++)
{
for (j = 0; j < GridNumber; j++)
{
Button btn = new Button();
btn.Height = 50;
btn.Width = 50;
// btn.Command = StartCommand; Ill add that later
btn.SetValue(Grid.RowProperty, i);
btn.SetValue(Grid.ColumnProperty, j);
if ((i + j) % 2 == 0)
{
btn.Background = bs[0];
}
else btn.Background = bs[1];
somebuttonlist.Add(btn);
}
}
}
Here I have a List of buttons, which should accept the new created buttons, then transfer them to the grid. Code of the button list:
private List<Button> _bslist = new List<Button>();
public List<Button> somebuttonlist
{
get
{
return _bslist;
}
set
{
_bslist = value;
NotifyPropertyChanged();
}
}
And xaml code:
<ItemsControl ItemsSource="{Binding somebuttonlist}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid Name="GameGrid" Visibility="{Binding GameVis}"
vm:GridHelpers.RowCount="{Binding RowCount}"
vm:GridHelpers.ColumnCount="{Binding ColumnCount}">
</Grid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Don't mind all the bindings, they work properly when I use the non-MVVM format.
So how do I properly transfer the working code into the View Model? Is there a way to access the children accordingly?
It looks like you are binding to a List, which does not notify when new records are inserted or any record is deleted. The NotifyPropertyChanged() that you have only kicks off when the reference to that list is modified, so for example, setting it to a new List() or a different List. Try changing it to an ObservableCollection instead, which sends an event for when the number of records in the collection get modified.
This is not the right way of doing things with MVVM. What you are trying to do now is to pull visual code into your ViewModel. Ideally, ViewModels should leave in a separate project without any references to visual stuff. This way, referencing Button will result in broken code unless you add a reference for System.Windows.Controls and this reminds you that you do things the wrong way.
What you are trying to do is doable in pure MVVM way, but will result in much more code, which also be convoluted to you if you haven't got a grasp on MVVM yet. Also note, that in real life, it is acceptable to have some code behind for complicated scenarios. For this you just create a custom control to encapsulate all code behind logic.
The nice solution in your case can be a custom control, that encapsulates Grid and logic to populate it with buttons. You also add dependency properties for Rows and Columns, so you can bind to them in XAML, and define a command which is invoked when button is clicked. This may be a good example: How to create bindable commands in Custom control?

Windows 10 UWP - GridView Items With no DataContext when Not Visible

I have an issue with a GridView in a UWP application that I'm working on...
Items in the GridView load correctly, however items that are out of view (off the page and not visible) do not have a DataContext assigned, and no event ever fires when the DataContext is assigned. Various bindings do work as TextBlocks that are bound get updated, but the the normal event workflow and Loaded events get all strange.
<GridView Grid.Row="1" Name="SearchGrid" ItemsSource="{Binding SearchItems}" ItemClick="SearchGrid_ItemClick">
<GridView.ItemTemplate>
<DataTemplate>
<local:RsrItemGridViewItem />
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
The grids all show correctly, except, for being able to properly delay load some items because the DataContext isn't set at time of load (and a DataContextChanged event isn't fired when the context is updated).
Does anyone have any ideas how to get notified when the control becomes visible? This seems like a notification bug, or there is some binding thing that I'm missing.
Thank you!
Does anyone have any ideas how to get notified when the control becomes visible?
You can't use FrameworkElement.Loaded event here to get notify when your RsrItemGridViewItem becomes visible, this event occurs when a FrameworkElement has been constructed and added to the object tree, and is ready for interaction.
GirdView control implements UI virtualization for better UI performance, if your GridView is bound to a collection of many items, it might download only items 1-50, When the user scrolls near the end of the list, then items 51 – 100 are downloaded and so on. But for example, there are only 20 items now be shown, but it might have loaded 45 items, 25 items could not be seen in this moment.
If you change the default ItemsPanel of GridView which is ItemsWrapGrid to for example VariableSizedWrapGrid, GridView will lose virtualization, and all items will be loaded at the same time even most of them can not be seen at one moment.
For you problem, I think what you can give a try is calculating the ScrollViewer's VerticalOffset with your GridView's height and the items's count be shown, and then you can know which items are been shown at this moment.
For example here:
private ObservableCollection<MyList> list = new ObservableCollection<MyList>();
public MainPage()
{
this.InitializeComponent();
this.Loaded += MainPage_Loaded;
}
private double viewheight;
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
var scrollViewer = FindChildOfType<ScrollViewer>(gridView);
scrollViewer.ViewChanged += ScrollViewer_ViewChanged;
viewheight = gridView.ActualHeight;
}
private void ScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
var scrollViewer = sender as ScrollViewer;
var Y = scrollViewer.VerticalOffset;
//calculate here to get the displayed items.
}
public static T FindChildOfType<T>(DependencyObject root) where T : class
{
var queue = new Queue<DependencyObject>();
queue.Enqueue(root);
while (queue.Count > 0)
{
DependencyObject current = queue.Dequeue();
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(current); i++)
{
var child = VisualTreeHelper.GetChild(current, i);
var typedChild = child as T;
if (typedChild != null)
{
return typedChild;
}
queue.Enqueue(child);
}
}
return null;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
list.Clear();
for (int i = 0; i < 200; i++)
{
list.Add(new MyList { text = "Item " + i });
}
}
Since GridView control's layout is adaptive to the app's size, the current displayed count is dynamic, you can try other height based properties (for example each item's height) and the ScrollViewer's VerticalOffset to calculate, there is no ready-made method to get your work done, it's a little complex to calculate, but I think there is no better solution for now.
After doing some testing with this, what I found out worked (though it's not very clean, and I believe there is a bug with bindings) was to add the custom control to the GridView, then in the grid view adding a DataContext={Binding} to the Image I wanted to get notified of an update on.
<UserControl ...><Image DataContext="{Binding}" DataContextChanged="ItemImage_DataContextChanged" /></UserControl>
The main control doesn't get notified of a DataContext change, but the child elements are notified.

Difficulty with tabControl/tabitem refresh

I have a WPF window with a maintabWindow and several tabitems.
It normally works fine and the layout is this:
but when I BEFORE add the following window:
the result is this:
So the problem is related with the tabControl/tabItem refresh.
This is fairly obvious but even more because if I move the window or pass with the mouse on the a tabItem they get refreshed one by one.
I searched and found that here is a solution: http://geekswithblogs.net/NewThingsILearned/archive/2008/08/25/refresh--update-wpf-controls.aspx
so I added:
this.MainTab.Refresh();
this.tabItem1.Refresh();
this.tabItem2.Refresh();
this.tabItem3.Refresh();
this.tabItem4.Refresh();
this.tabItem5.Refresh();
but that didn't change a thing.
Thanx for any help
Ok so in the end it has a quite a weird behavious. If I do
for (int i = 0; i < tbcMain.Items.Count; i++)
{
tbcMain.SelectedIndex = i;
tbcMain.UpdateLayout();
}
it works. But I have to set the 1st tabitem so if I add
tbcMain.SelectedIndex = 0;
it doesn't.
So the solution was put a sleep and it works again.
for (int i = 0; i < tbcMain.Items.Count; i++)
{
tbcMain.SelectedIndex = i;
tbcMain.UpdateLayout();
}
System.Threading.Thread.Sleep(250);
tbcMain.SelectedIndex = 0;
But that is not elegant at all. If anyone has a better solution pls let me know it.
Btw adding the tbcMain.SelectedIndex = 0; on the loaded event of the mainWindow is of no use.
You should be able to set your SelectedIndex first, and without having to include it in your loop:
tbcMain.SelectedIndex = 0;
Then, basing it off of your response, you should be able to either just do .UpdateLayout() on each of your TabItems:
MainTab.UpdateLayout();
tabItem1.UpdateLayout();
tabItem2.UpdateLayout();
tabItem3.UpdateLayout();
tabItem4.UpdateLayout();
tabItem5.UpdateLayout();
Or you should be able to do something like this in your loop:
MainTab.UpdateLayout();
for (int i = 0; i < tbcMain.Items.Count; i++)
{
TabItem tbi = (TabItem)this.FindControl("tabItem"+i);
tbi.UpdateLayout();
}
Updating/refreshing should have nothing to do with setting the selected one. Including the selection of the tab within the loop to i was your problem - not a race condition. Set the tbcMain.SelectedIndex = 0 outside of your loop and you should be fine. Sometimes, however, this doesn't work and you need to set it with Dispatcher:
Dispatcher.BeginInvoke((Action)(() => this.tbcMain.SelectedIndex = 0));
There's a write up/comments on a separate thread regarding why it needs to be sent to Dispatcher:
How to programmatically select a TabItem in WPF TabControl
Though, unfortunately for me, I had a similar issue where I was trying to refresh a ListView on a subtab. Neither .UpdateLayout(), nor .InvalidateVisual() (as I saw on this thread) worked. I just had to rebind my grid in the button event I was using on my main page, so that when the tab was clicked, it was refreshed manually. I added an x:Name property on the tab so I could call it using "dot" syntax, and it exposed the ListView. I simply added the DataTable of results back to that ListView's DataContext.

How to remove previously updated UIElement on grid row to add new UIElement?

I am working in silverlight where i have a grid called "bigGrid" which contains three rows and on the first row i have combobox and i update two UI elemnt on two different rows on Loading the comboBox (i mean second row bigGrid(because bigGrid is parent of all) or first and second row of rowGrid in my code).
Now on selectionChanged Event of combobox i have to replace the previously rendered UI elemnts with the UI element selected from combo box (If the UIelement is one it will be displayed on one row and if the UI elements are two they will be displayed on two different rows one after other (Please note that on Loading this combobox i display 2 UI elemnts in 2 consecutive rows.)
Now Problem ?: The problem in when i load combobox the grids are intialised with two UIelement on 2 rows. but on Selectionchnaged event when i render 2 uielements then it wroks fine for 2 UIElements (it replaces the previous rendering of Loaded event on both rows . But the problem is when i dispaly only 1 UIElement in first row because MY RECENTLY RENDERED uiElement on selection changed event is no doubt updated in first row but the UI element of second row (from combobox Loaded even still persists).
How to delete this PREVIOUSLY persisting UI element ?
My code for (please note that i have given just useful code. Ofcourse combe is declared somewhere and it has Items as well)
public Grid somefunction() //this function returs the final bigGrid (which contains all the ROWS CONATINING ui ELEMNTS)
{
cmb.Loaded += (o3, e) =>
{
foreach (object o in pv.Root.Parameter)
{
param = (Parameter)o;
if (o is Parameter)
{
rowGrid = IntializeUIElements(param, atrbt);
}
Grid.SetRow(rowGrid, loopCount);
}
bigGrid.Children.Add(rowGrid);
loopCount++;
};
cmb.SelectionChanged += (o1, e) =>
{
string selectedComboItem = cmb.SelectedItem.ToString();
Grid storeRowGrid = new Grid();
for (int i = 0; i < pv.Root.Parameter.Count; i++)
{
storeRowGrid.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
}
int count = 0;
foreach (object o in pv.Root.Parameter)
{
if (o is Parameter)
{
rowGrid = IntializeUIElements(param, atrbt); //It returns the grid with UI element
Grid.SetRow(rowGrid, count);
storeRowGrid.Children.Add(rowGrid);
Grid.SetRow(storeRowGrid, count);
if (bigGrid.Children.Count > 1)
{
bigGrid.Children.RemoveAt(bigGrid.Children.Count - 1); //this is to remocve previous item on selection change
}
count++;
}
}
bigGrid.Children.Add(storeRowGrid);
};
Grid.SetColumn(cmb, 1);
comboRowGrid.Children.Add(cmb);
Grid.SetRow(comboRowGrid, 0);
bigGrid.Children.Add(comboRowGrid); //This BigGrid is parent Grid.
return bigGrid;
}
How to clear the previous UI element of Loaded event on second row when selection changed event show just
& uielement in first row ?
Your layout is something like this:
<Grid>
<ComboBox Grid.Row="0"/>
<SomeControl Grid.Row="1"/>
<SomeOtherControl Grid.Row="2"/>
...
</Grid>
If you made it more like this:
<Grid>
<ComboBox Grid.Row="0"/>
<Grid x:Name="grid with controls chosen by the Combobox selection" Grid.Row="1">
<SomeControl Grid.Row="0"/>
<SomeOtherControl Grid.Row="1"/>
</Grid>
...
</Grid>
You wouldn't have to worry about how many controls to remove, because you can just remove/replace a single grid.

DataGrid Cell.Style binding

I have a performance issue with the WPF DataGrid (.net 4.0)
first, some details:
I have a datagrid with an Observable collection as ItemsSource.
this observableCollection itself contains collections of objects, each collection hence being a row, each object being a cell ("logical" cell of course, not actual dataGridCell)
the reason why I do this is because I only know at runtime how many columns I will have in my dataGrid.
then I bind each DataGridCell's value to the value of the object in the "logical" table (= the collection of collections)
now the trouble I have is that I also have to be able to change whatever cell's Properties (like Background, Foreground, FontFamily, etc...) at any time while the app is running.
The solution I came up with is one involving setting the columns' cellStyles with bindings that bind to the "logical" cells' properties
here Is a sample code (no Xaml in my app):
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Width = 1200;
Height = 780;
Top = 60;
Left = 200;
DataGrid dg = new DataGrid();
Content = dg;
ObservableCollection<Row> Source = new ObservableCollection<Row>();
dg.ItemsSource = Source;
dg.SelectionMode = DataGridSelectionMode.Extended;
dg.IsSynchronizedWithCurrentItem = true;
dg.CanUserSortColumns = false;
dg.CanUserReorderColumns = true;
dg.CanUserResizeColumns = true;
dg.CanUserResizeRows = true;
dg.CanUserAddRows = false;
dg.CanUserDeleteRows = false;
dg.AutoGenerateColumns = false;
dg.EnableColumnVirtualization = true;
dg.EnableRowVirtualization = false; // unuseful in my case : I alawys have less lines than the DG can contain
dg.VerticalScrollBarVisibility = ScrollBarVisibility.Visible;
dg.GridLinesVisibility = DataGridGridLinesVisibility.None;
dg.HorizontalGridLinesBrush = Brushes.LightGray;
dg.MinRowHeight = 20;
dg.RowHeaderWidth = 20;
for (int i = 0; i < 100; i++)
{
DataGridTextColumn column = new DataGridTextColumn();
column.Binding = new Binding(String.Format(CultureInfo.InvariantCulture, "[{0}].Text", i));
Style style = new Style(typeof(DataGridCell));
style.Setters.Add(new Setter(DataGridCell.BackgroundProperty, new Binding(String.Format(CultureInfo.InvariantCulture, "[{0}].Background", i))));
style.Setters.Add(new Setter(DataGridCell.ForegroundProperty, new Binding(String.Format(CultureInfo.InvariantCulture, "[{0}].Foreground", i))));
column.CellStyle = style;
column.Header = "Column " + i;
dg.Columns.Add(column);
}
for (int i = 0; i < 35; i++)
{
Row dgRow = new Row();
Source.Add(dgRow);
for (int j = 0; j < 100; j++)
dgRow.Add(new TextBox() { Text = "cell " + i + "/" + j, Background = Brushes.AliceBlue, Foreground = Brushes.BlueViolet });
}
}
}
public class Row : ObservableCollection<TextBox>
{
}
my problem is: with the VolumnVirtualisation On (I don't need row Virtualization in my case), the grid takes about 2sec to load, and then 1sec each time I move the horizontal scrollbar by a big leap (clic in the scrollBar bg, not the arrow)
this is too much for my purpose
so my question is: am I doing something wrong and if yes, what? what better way to do this do I have?
thanks for reading
If ColumnVirtualization make so problems, why do you need it?
You can do a several improvements, but they can't solve the problem completely.
Change TextBoxes for light-weight objects:
public class TextItem
{
public string Text { get; set; }
public Brush Background { get; set; }
public Brush Foreground { get; set; }
}
public class Row : ObservableCollection<TextItem>
{
}
Enable VirtualizingStackPanel: dg.SetValue(VirtualizingStackPanel.IsVirtualizingProperty, true);
Replace styles with templates:
for (int i = 0; i < 100; i++)
{
DataGridTemplateColumn column = new DataGridTemplateColumn();
column.CellTemplate = (DataTemplate)XamlReader.Parse(
"<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>" +
"<TextBlock DataContext='{Binding [" + i + "]}' Text='{Binding Text}' Background='{Binding Background}' Foreground='{Binding Foreground}'/>" +
"</DataTemplate>");
column.Header = "Column " + i;
dg.Columns.Add(column);
}
After a lot of time put into this, I came to the conclusion that I've reached the limit.
Here are a few thoughts for those that are dealing with the same issue:
There is no easy way to manage a single cell's visual properties in WPF as of .net 4.0: MS did not plan anything to make this easy so basically you are stuck with 2 possibilities to do this:
get the actual dataGridCell using some kind of helper function and then change its properties directly. This is easily done but can lead to big trouble if virtualization is turned on.
bind each cell's visual properties to dependency properties from your VM inside the dataGridCell's Style. you can use either the DataGrid.CellStyle or the Column.CellStyle to do so, depending on you constraints. This slows the dataGrid quite a bit though, and is quite a hassle to manage.
if like me you have no choice but to use the second option (because I need virtualization), here are a few things to consider:
you are not stuck with C#. There is actually a way to do your CellStyle in Xaml. See Martino's post on this issue. As far as I'm concerned, this works pretty well. I tweaked it a bit so as not to have to use the hack though: I define my style in Xaml and apply it to the Column.CellStyle. Then when I create a column in my code behind, I simply create a new Style inheriting this one, and I add the Tag setter with a binding set to: "[column's Index].Self". This breaks the MVVM model, but I'm not using it anyway and It's easier to maintain like this.
obviously, the more properties you have to bind, the more time it will take for your dataGrid to load, so stick to the minimum (using light-weight objects does make a small difference, as stated by Vorrtex).
while using templates or styles makes absolutely no difference regarding performance, if you are using dataGridTemplateColumns, you'd want to set you bindings up directly in the template instead of adding a style on top of the template, obviously (this does make a small difference with a huge number of data)
if anybody has anything to add to this, please do so! I'm still looking for any idea that can improve things up and would be glad for whatever crazy idea you have on the subject. Even in 3 months...

Categories

Resources