I'm using dxGrid in a xamarin application to display contents. This works well so far, but I can't get the grid to update values.
I'm using this grid:
<dxGrid:GridControl
x:Name="grid"
ItemsSource="{Binding articles, Mode=TwoWay}"
SelectedDataObject="{Binding selectedRowObject}"
AutoFilterPanelVisibility="true"
IsReadOnly="true"
SortMode="Multiple">
<dxGrid:GridControl.Columns>
<dxGrid:TextColumn
FieldName="description"
Caption = "Description"
AutoFilterCondition="Contains"/>
<dxGrid:TextColumn
FieldName="id"
Caption = "ID"
AutoFilterCondition="Contains"/>
<dxGrid:TextColumn
FieldName="formattedPrice"
Caption = "Price"
AutoFilterCondition="Contains"/>
</dxGrid:GridControl.Columns>
</dxGrid:GridControl>
In my ViewModel, I use this for the databinding:
public List<Article> articles
{
get
{
return dataItem.articles;
}
}
The dataItem object contains a list of articles besides some other general information. When I open the page, the contents show up correctly, but when I'd like to be able to do this manually. For example, this does not work when I'm returning to the oage via Navigation.PopAsync();.
To reload the list, I use PropertyChanged(this, new PropertyChangedEventArgs(nameof(articles)));, the pendant of this works for all other elements but the grid. The items of the articles-object are correctly set though when I check them in the debugger, they are just not updating.
I found a quick fix that works for me (altough it looks kind of unclean):
In the code-behind of the view:
protected override void OnAppearing()
{
grid.RefreshData();
base.OnAppearing();
}
Related
I have a form that has a dynamic amount of datagrids that are brought in programmatically each one on a new tabpage.
My problem is that I need to change the Header of each column. I have tried doing it through a method
DataGridForSupplier.Columns[0].Header = "123";
but that keeps crashing with an error:
Index was out of range. Must be non-negative and less than the size of the collection
Turns out the problem is that the grid wasn't finished loading. So after waiting for all tabpage to load and add data to all the grids , even then the code
DataGridForSupplier.Columns[0].Header = "123";
would still crash. If the tabs are left to load on their own with no header tampering then the datagrid shows fine.
I would just LOVE to do this in XAML problem is that seeing that I don't know how many grids will load at run time I tried doing this at the back. So I'm open to any solution at this point. I tried finding a solution that would incorporate something that would 'theme' all the datagrids. Luckily all the datagrids headers will repeat across all tabs. So header 1 on tabpage 1 - 10 will be the same. Header 2 on tabpage 1 - 10 will be the same
Something like
<DataGridTemplateColumn.Header>
<TextBlock Text="{Binding DataContext.HeaderNameText, RelativeSource=>> RelativeSource AncestorType={x:Type DataGrid}}}" />
</DataGridTemplateColumn.Header>
but this needs to repeat for every Grid. This seems to escape me at the moment.
Any help would be welcome.
A rather lengthy answer, but this solution does not require any additional libraries, 3rd party tools, etc. You can expand it as you want later such as for adding hooks to mouse-move/over/drag/drop/focus, etc. First the premise on subclassing which I found out early in my learning WPF. You can not subclass a xaml file, but can by a .cs code file. In this case, I subclassed the DataGrid to MyDataGrid. Next, I created an interface for a known control type to ensure contact of given functions/methods/properties. I have stripped this version down to cover just what you need to get.
The interface below is just to expose any class using this interface MUST HAVE A METHOD called MyDataGridItemsChanged and expects a parameter of MyDataGrid.. easy enough
public interface IMyDataGridSource
{
void MyDataGridItemsChanged(MyDataGrid mdg);
}
Now, declaring in-code a MyDataGrid derived from DataGrid. In this class, I am adding a private property of type IMyDataGridSource to grab at run-time after datagrids are built and bound.
public class MyDataGrid : DataGrid
{
// place-holder to keep if so needed to expand later
IMyDataGridSource boundToObject;
public MyDataGrid()
{
// Force this class to trigger itself after the control is completely loaded,
// bound to whatever control and is ready to go
Loaded += MyDataGrid_Loaded;
}
private void MyDataGrid_Loaded(object sender, RoutedEventArgs e)
{
// when the datacontext binding is assigned or updated, see if it is based on
// the IMyDataGridSource object. If so, try to type-cast it and save into the private property
// in case you want to add other hooks to it directly, such as mouseClick, grid row changed, etc...
boundToObject = DataContext as IMyDataGridSource;
}
// OVERRIDE the DataGrid base class when items changed and the ItemsSource
// list/binding has been updated with a new set of records
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
// do whatever default behavior
base.OnItemsChanged(e);
// if the list is NOT bound to the data context of the IMyDataGridSource, get out
if (boundToObject == null)
return;
// the bound data context IS of expected type... call method to rebuild column headers
// since the "boundToObject" is known to be of IMyDataGridSource,
// we KNOW it has the method... Call it and pass this (MyDataGrid) to it
boundToObject.MyDataGridItemsChanged(this);
}
}
Next into your form where you put the data grid. You will need to add an "xmlns" reference to your project so you can add a "MyDataGrid" instead of just "DataGrid". In my case, my application is called "StackHelp" as this is where I do a variety of tests from other answers offered. The "xmlns:myApp" is just making an ALIAS "myApp" to the designer to it has access to the classes within my application. Then, I can add
<Window x:Class="StackHelp.MyMainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:myApp="clr-namespace:StackHelp"
Title="Main Window" Height="700" Width="900">
<StackPanel>
<!-- adding button to the main window to show forced updated list only -->
<Button Content="Refresh Data" Width="100"
HorizontalAlignment="Left" Click="Button_Click" />
<myApp:MyDataGrid
ItemsSource="{Binding ItemsCollection, NotifyOnSourceUpdated=True}"
AutoGenerateColumns="True" />
</StackPanel>
</Window>
Now, into the MyMainWindow.cs code-behind
namespace StackHelp
{
public partial class MyMainWindow : Window
{
// you would have your own view model that all bindings really go to
MyViewModel VM;
public MyMainWindow()
{
// Create instance of the view model and set the window binding
// to this public object's DataContext
VM = new MyViewModel();
DataContext = VM;
// Now, draw the window and controls
InitializeComponent();
}
// for the form button, just to force a refresh of the data.
// you would obviously have your own method of querying data and refreshing.
// I am not obviously doing that, but you have your own way to do it.
private void Button_Click(object sender, RoutedEventArgs e)
{
// call my viewmodel object to refresh the data from whatever
// data origin .. sql, text, import, whatever
VM.Button_Refresh();
}
}
}
Finally to my sample ViewModel which incorporates the IMyDataGridSource
public class MyViewModel : IMyDataGridSource, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged(string propertyName)
{ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
public ObservableCollection<OneItem> ItemsCollection { get; set; }
= new ObservableCollection<OneItem>();
public void Button_Refresh()
{
ItemsCollection = new ObservableCollection<OneItem>
{
new OneItem{ DayName = "Sunday", DayOfWeek = 0},
new OneItem{ DayName = "Monday", DayOfWeek = 1},
new OneItem{ DayName = "Tuesday", DayOfWeek = 2},
new OneItem{ DayName = "Wednesday", DayOfWeek = 3},
new OneItem{ DayName = "Thursday", DayOfWeek = 4},
new OneItem{ DayName = "Friday", DayOfWeek = 5 },
new OneItem{ DayName = "Saturday", DayOfWeek = 6 }
};
RaisePropertyChanged("ItemsCollection");
}
// THIS is the magic hook exposed that will allow you to rebuild your
// grid column headers
public void MyDataGridItemsChanged(MyDataGrid mdg)
{
// if null or no column count, get out.
// column count will get set to zero if no previously set grid
// OR when the items grid is cleared out. don't crash if no columns
if (mdg == null)
return;
mdg.Columns[0].Header = "123";
}
}
Now, taking this a step further. I don't know how you manage your view models and you may have multiple grids in your forms and such. You could create the above MyViewModel class as a smaller subset such as MyDataGridManager class. So each datagrid is bound to its own MyDataGridManager instance. It has its own querying / populating list for the grid, handling its own rebuild column headers, mouse clicks (if you wanted to expand), record change selected, etc.
Hope this helps you some. Again, this does not require any other 3rd party libraries and you can extend as you need. I have personally done this and more to the data grid and several other controls for certain specific pattern handling.
I have an ASP.NET Web API which works all fine according to my tests. Now, I want to consume it in a simple WPF application. I just want to get the data from API, display it in the client app, and then to add or delete the data from the database just by using the client app.
My problem is that when I pass the data to a ListBox, I kind of lose information, specifically I lose the Id of the object, which I need later in order to delete it from the database. In my scenario, I have something like this in one of my pages:
<ListBox
Name="carCategoryListBox"
Grid.Row="2"
Grid.Column="1" />
And then, in the .cs file of the page, I do something like this:
private void Page_Loaded(object sender, RoutedEventArgs e)
{
BindCarCategoryList();
}
private async void BindCarCategoryList()
{
var client = CarHttpClient.GetClient();
HttpResponseMessage response = await client.GetAsync("api/carcategories");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var carCategories = JsonConvert.DeserializeObject<List<DTO.CarCategory>>(content);
carCategoryListBox.ItemsSource = carCategories.Select(cc => cc.Category).ToList();
}
else
{
MessageBox.Show("Error code " + response.StatusCode + "\nMessage - " + response.ReasonPhrase);
return;
}
}
As you can see I just get the Category fields of my objects, and pass them to the ListBox to be displayed. The CarCategory class is pretty simple and can be seen below:
public class CarCategory
{
public int Id { get; set; }
public string Category { get; set; }
}
The problem is that at a later stage I need to get the actual Id of the object, and use it. But, since I pass only the Category field to the ListBox, this Id information is somehow lost in my case. If I do something like this:
int id = carCategoryListBox.SelectedIndex;
Then, I just get the Id of the item in the ListBox (which item it is in the list), I don't actually get the original Id of the object that is stored in the database. Any ideas how to pass the data to a ListBox in such a way that I still somehow preserve the original Id information?
ListBox.SelectedIndex is a position of selected item in ItemsSource. It can even be -1 if there is no selected item. And it is not the same value as Id
to get databound object make some changes:
1) use List<CarCategory> as ItemsSource:
carCategoryListBox.ItemsSource = carCategories;
2) set ListBox.DisplayMemberPath for it to display category name:
<ListBox DisplayMemberPath="Category"
Name="carCategoryListBox"
Grid.Row="2"
Grid.Column="1" />
3) use SelectedItem property to get selected item (can be null):
var category = (CarCategory)carCategoryListBox.SelectedItem;
You should bind the entire object not select just a field from it, and set the list box's DisplayMemberPath to 'Category'. Then your selected item will be the entire CarCategory, and you can get its ID.
You should also look at MVVM and try to get as close as possible to the concepts, they really help.
You have to change your BindCarCategoryList
carCategoryListBox.ItemsSource = carCategories.Select(cc => cc.Category).ToList();
to
carCategoryListBox.ItemsSource = carCategories;
and modify your xaml like this
<ComboBox Grid.Row="2" Grid.Column="1" Margin="4" Width="250" DisplayMemberPath="Category" DisplayMemberPath="Id" />
or if you dont use MVVM assign DisplayMemberPath and DisplayMemberPath directly in your .cs file
All I wanted is something like a Linq Distinct available to either show duplicates or not, depending on the state of a Checkbox.
this is the XAML (with all properties like names, and everything regarding to layout removed, to avoid distraction):
<ScrollViewer>
<ListView SelectionMode="Single"/>
</ScrollViewer>
<CheckBox Content="group duplicates" Checked="CheckBox_Checked" Unchecked="CheckBox_Unchecked"/>
Each item of my list view is a Label with a Hyperlink as it's content, that opens the Uri set in the NavigateUri Property of the Hyperlink with the default internet browser when clicked.
Some of the links appear more than once when the application is run, wich is relevant because it shows the number of occurencies of those uri. I can't get rid of the duplicates when building the list either, because it is ordered by the time at which each uri occurred, and that is also relevant.
Because it is hard for the human eye to distinguish between each uri because of the fact that they look alike a lot, it would be practical to be able to do two things:
annotate in the link the number of times that uri occurs in the listview even when it is showing the duplicates
to collapse the duplicates into one link (with the number of occurencies properly annotated)
The result I'm hoping to achieve would be something like this:
(displaying duplicates)
uri1 (2)
uri2 (3)
uri1 (2)
uri3 (1)
uri2 (3)
uri2 (3)
(grouping duplicates)
uri1 (2)
uri2 (3)
uri3 (1)
This is how I populate the listvew in the code behind at the moment (irrelevant portions not shown to avoid distraction)
foreach (string uriStr in uriStrCollection)
{
Uri uri = new Uri(uriStr);
Label lbl = new Label();
Hyperlink link = new Hyperlink();
link.Inlines.Add(uriStr);
link.NavigateUri = uri;
link.RequestNavigate += link_RequestNavigate;
lbl.Content = link;
listView.Items.Add(lbl);
}
To store uriStrCollection is not a good solution, because it's large and it would be basically storing the same data twice.
To generate uriStrCollection again is an even worse idea, because the process involves a lot more memory and computation.
I tried to use Linq extension methods on ListView.Items and ListView.Items.SourceCollection, but I can't: Linq methods don't even show on auto-complete, wich is very weird to me, specially because the compile time type of SourceCollection is IEnumerable.
Any ideas?
Since you're using WPF, let's start by doing this the WPF way - separate the data from the UI; the UI should be used to display the data, not store it.
Forget the UI for now. First, create the classes and data structures you need to store a list of URIs and their occurrences in code, for example:
public class UriWithOccurrence
{
public string UriString { get; set; }
public int Occurrences { get; set; }
}
...
//Keep a global collection of URIs:
_allUris = new List<UriWithOccurrence>();
foreach (string uriStr in uriStrCollection)
{
var item = new UriWithOccurrence()
{
UriString = uriStr,
Occurrences = uriStrCollection.Count(s => (s == uriStr))
};
_allUris.Add(item);
}
Now we start thinking about displaying these URIs in the UI an create a ListView that can list UriWithOccurrences. Note the DisplayMemberBindings which point to properties in UriWithOccurrence:
<ListView x:Name="_list" >
<ListView.View>
<GridView>
<GridViewColumn Header="Link" DisplayMemberBinding="{Binding UriString}" />
<GridViewColumn Header="#" DisplayMemberBinding="{Binding Occurrences}" />
</GridView>
</ListView.View>
</ListView>
To connect the dots and see some URIs, we set the ListView's ItemsSource to our URI collection:
_list.ItemsSource = _allUris;
This article explains all this more in-depth: http://tech.pro/tutorial/742/wpf-tutorial-using-the-listview-part-1
To get clickable URIs, this MSDN question may be helpful: WPF ListView "hyperlink url" column
Finally, to get the "Group duplicates" checkbox working, a quick and dirty solution could be:
private void CheckBox_Checked(object sender, EventArgs e)
{
_list.ItemsSource = _allUris;
}
private void CheckBox_UnChecked(object sender, EventArgs e)
{
_list.ItemsSource = _allUris.GroupBy(u => u.UriString)
.Select(gr => gr.First());
}
A final tip:
To really do this the WPF way, read up on the MVVM pattern and put most of this code in a ViewModel.
I have a DataGrid that gets its data updated every few seconds via a Thread. The DataGrid needs to offer Column Header sorting, grouping and filtering.
I currently have a DataGrid bound to a ICollectionView and the source of the ICollectionView is an ObservableCollection. Which seems to be the good way to do it from what I read on other threads.
The sort-ing "works" but it gets lost when the ICollectionView.Source gets updated following an update of the ObservableCollection. I have tried saving the SortDescriptions before the update and re-add it to the ICollectionView after the update is done. But it's the same result.
May someone point me to what I'm missing?
Edit Here's some code...
View (XAML)
<DataGrid ItemsSource="{Binding CollectionView, Source={StaticResource ViewModel}}>
ViewModel
public ICollectionView CollectionView
{
get
{
collectionViewSource.Source = dataColl;
if (SortDescriptions != null)
{
foreach (SortDescription sd in SortDescriptions)
{
collectionViewSource.View.SortDescriptions.Add(sd);
}
}
collectionViewSource.View.Refresh();
return collectionViewSource.View;
}
}
public ObservableCollection<SomeObject> DataColl
{
get { return dataColl; }
private set
{
this.dataColl= value;
OnPropertyChanged("CollectionView");
}
}
Following is the method that updates the data every few seconds...
private void UpdateData()
{
while (true)
{
System.Threading.Thread.Sleep(mDataRefreshRate);
// SortDescriptions is a Property of the ViewModel class.
SortDescriptions = collectionViewSource.View.SortDescriptions;
ObservableCollection<SomeObject> wDataColl
= new ObservableCollection<SomeObject>();
//... Irrelevant code that puts the data in wDataColl ...
DataColl= wDataColl;
}
}
[YourObservableCollection].ViewHandler.View.Filter
+= new FilterEventHandler(myFilterHandler);
private void myFilterHandler(object sender, FilterEventArgs e)
{
}
Can be used to directly add your filter handler and you can do the same with SortDescriptions to Add/Remove
[YourObservableCollection].ViewHandler.View.SortDescriptions.Add(mySortDescription);
If you are doing allot of sorting and filtering best to create own class encapsulating a CollectionViewSource and implement adding and removing SortDescriptions and Filtering etc
When you say:
The sort "works" but it gets lost when the ICollectionView.Source gets
updated following an update of the ObservableCollection
What do you mean by update? you mean you are changing the Source? Rather than adding/removing items from the collection?
EDIT based on your XAML example you added:
<DataGrid ItemsSource="{Binding CollectionView, Source={StaticResource ViewModel}}>
You are binding itemsource to the CollectionViewSource where you should bind the datacontext to it:
Example:
<Page.Resources>
<CollectionViewSource x:Key="myViewSource"
Source="{Binding CollectionView, Source={StaticResource ViewModel}}"
/>
</Page.Resources>
In page:
<Grid DataContext="{StaticResource myViewSource}">
<DataGrid x:Name="myGrid" ItemsSource="{Binding}"...
Or something along those lines
EDIT again didnt see code scroll down :p
ObservableCollection<SomeObject> wDataColl= new ObservableCollection<SomeObject>();
You create new instance every time of collection lol this is main problem
Also:
public ICollectionView CollectionView
{
get
{
collectionViewSource.Source = dataColl;
if (SortDescriptions != null)
{
foreach (SortDescription sd in SortDescriptions)
{
collectionViewSource.View.SortDescriptions.Add(sd);
}
}
collectionViewSource.View.Refresh();
return collectionViewSource.View;
}
}
Here where you return collection you are setting the Source and adding the SortDescriptions and also refreshing the view every time, you only need set these values once
You would only call refresh on the View if you add/remove SortDescriptions
I think you should get to grips with basics of CollectionViewSource
The problem is that you swap out the entire ObservableCollection every time you add new data.
ObservableCollection<SomeObject> wDataColl= new ObservableCollection<SomeObject>();
... Unrelevant code that puts the data in wDataColl ...
DataColl= wDataColl;
Make sure to use Add to the existing collection instead (perhaps after using Clear() first if that is necessary)... If you still have problems after that please comment and i will try to help.
Also, try to avoid using the Refresh() as it rebuilds the entire view and is unnecessarily expensive. If you do sorting, adding, removing etc. the correct way use of Refresh() isn't needed.
I know I am missing something here and I could use a pointer. Within a project I have an expander control when this control is clicked it makes a RIA call to a POCO within my project to retreive a second set of data. I am using the SimpleMVVM toolkit here so please let me know if I need to expand on any additional areas.
Within the xaml the expander is laid out as
<toolkit:Expander Header="Name" Style="{StaticResource DetailExpanderSytle}" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="Expanded">
<ei:CallMethodAction
TargetObject="{Binding Source={StaticResource vm}}"
MethodName="showWarrantNameDetail"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<StackPanel Orientation="Vertical">
<sdk:DataGrid AutoGenerateColumns="true" ItemsSource="{Binding NameResult}" AlternatingRowBackground="Gainsboro" HorizontalAlignment="Stretch" MaxHeight="200">
</sdk:DataGrid>
<local:NameContainer DataContext="{Binding}" />
</StackPanel>
</toolkit:Expander>
I am using the expression Dll coupled with Simple MVVM to get at the methods in the view model vs commands.
Within the view model I have the following code
public void showWarrantNameDetail()
{
//set flags
IsBusy = true;
CanDo = false;
EntityQuery<WarrantNameDataView> query = App.cdContext.GetWarrantNameDataViewsQuery().Where(a => a.PrimaryObjectId == Convert.ToInt32(RecID));
Action<LoadOperation<WarrantNameDataView>> completeProcessing = delegate(LoadOperation<WarrantNameDataView> loadOp)
{
if (!loadOp.HasError)
{
processWarrantNames(loadOp.Entities);
}
else
{
Exception error = loadOp.Error;
}
};
LoadOperation<WarrantNameDataView> loadOperation = App.cdContext.Load(query, completeProcessing, false);
}
private void processWarrantNames(IEnumerable<WarrantNameDataView> entities)
{
ObservableCollection<WarrantNameDataView> NameResult = new ObservableCollection<WarrantNameDataView>(entities);
//we're done
IsBusy = false;
CanDo = true;
}
When I set a break on the processWarrantName I can see the NameResult is set to X number of returns. However within the view the datagrid does not get populated with anything?
Can anyone help me understand what I need to do with the bindings to get the gridview to populate? Other areas of the form which are bound to other collections show data so I know I have the data context of the view set correctly. I've tried both Data context as well as Items Source and no return?
When I set a break on the code the collection is returned as follows so I can see that data is being returned. Any suggestions on what I am missing I would greatly appreciate it.
With regards to the page datacontext I am setting it in the code behind as follows:
var WarrantDetailViewModel = ((ViewModelLocator)App.Current.Resources["Locator"]).WarrantDetailViewModel;
this.DataContext = WarrantDetailViewModel;
this.Resources.Add("vm", WarrantDetailViewModel);
Thanks in advance for any suggestions.
Make ObservableCollection<WarrantNameDataView> NameResult a public property of your ViewModel class. Your view will not be able to bind to something that has a private method scope (or public method scope, or private member scope).
//declaration
public ObservableCollection<WarrantNameDataView> NameResult { get; set }
//in the ViewModel constructor do this
NameResult = new ObservableCollection<WarrantNameDataView>();
//then replace the original line in your method with:
//EDIT: ObservableCollection has no AddRange. Either loop through
//entities and add them to the collection or see OP's answer.
//NameResult.AddRange(entities);
If processWarrantNames gets called more than once, you might need to call NameResult.Clear() before calling AddRange() adding to the collection.
Phil was correct in setting the property to public. One note I'll add is there is no AddRange property in SL or ObservableCollection class that I could find. I was able to assign the entities to the OC using the following code
private ObservableCollection<WarrantNameDataView> warrantNameResult;
public ObservableCollection<WarrantNameDataView> WarrantNameResult
{
get { return warrantNameResult; }
set
{
warrantNameResult = value;
NotifyPropertyChanged(vm => vm.WarrantNameResult);
}
}
and then within the return method
WarrantNameResult = new ObservableCollection<WarrantNameDataView>(entities);
This worked and passed to the UI the collection of data.