Using ReactiveUI 6.5, I'm trying achieve something like a SelectMany LINQ expression with the CreateDerivedCollection feature of RxUI. The objects in my source collection (type 'A') have an IsSelected property, as well as another collection property where the items are of type 'B'. I want to end up with an IReactiveDerivedList<B> which is a flattened list of all of the B objects from the A's that are selected. Hopefully that makes sense.
To make it a little more concrete, let's use an example of an app for browsing log files. Say we have a LogFileViewModel type and our main screen's view model has a list of these. Each instance in the list represents a log file on the system, and we present a list of these that the user can select. It's a multi-select list so they can select more than one at a time.
The LogFileViewModel then has an IsSelected boolean property, which will be set to true/false as the user selects or deselects the corresponding item in the list. And it has a property which is a List<LogEntry>. Each LogEntry object of course represents one entry in the corresponding log file.
What I want to do then is have a reactive list in the main view model which is a list of all of the LogEntry objects for all of the currently selected LogFileViewModel objects. The ReactiveList of selected log files is easy, but I'm stuck on the second part. Here's what the main view model would basically look like:
public class MainViewModel
{
public MainViewModel()
{
//This gets initialized with the log files somehow, doesn't matter
LogFiles = new List<LogFileViewModel(...);
SelectedLogFiles = LogFiles.CreateDerivedCollection(l => l, l => l.IsSelected);
SelectedLogFileEntries = ? //How to create this one?
}
public List<LogFileViewModel> LogFiles { get; private set; }
public IReactiveDerivedList<LogFileViewModel> SelectedLogFiles { get; private set; }
public IReactiveDerivedList<LogEntry> SelectedLogFileEntries { get; private set; }
}
Is there a known way to do this that I'm just not seeing? If not, any clever ideas to achieve this behavior? :-)
Edit :
Looks like I missed this question in my initial search. Paul provided the "clever" solution to this problem about 2 years ago. So my question now ... is this still the best way to achieve this behavior?
Related
This should be a duplicate question but I'm posting it because none of the answers anywhere are working.
I have a dictionary of the types:
private Dictionary<IModule, AssemblyLoadContext> ModuleList = new Dictionary<IModule, AssemblyLoadContext>();
I am trying to bind the names of the IModules (IModule.Handle, for everything that implements IModule) to a combobox.
I've tried many things and searched through every answer on google but nothing works. This is apparently the way you are supposed to do it:
comboBox1.DataSource = new BindingSource(ModuleList, null);
comboBox1.DisplayMember = "Value";
comboBox1.ValueMember = "Key";
When I do this I get a RUNTIME error: (System.ArgumentException: 'Cannot bind to the new display member. (Parameter 'newDisplayMember')'
)
When I try swapping key and value I get this same error: (System.ArgumentException: 'Cannot bind to the new display member. (Parameter 'newDisplayMember')'
)
When I try other combinations of key/value, I get random results. Sometimes it will show the entire class name (not helpful), sometimes it will show the ToString representation (overloaded and works perfectly except doesn't UPDATE after startup), and sometimes it just shows nothing or the program gives an error during runtime.
However no combination of things I have tried actually gets the BOX contents to UPDATE when modules are loaded and unloaded (the modules themselves are definitely loading/unloading and work fine).
This is supposedly working as of many years ago and I can only imagine microsoft broke something in one of their updates because the intended method does NOT work for me.
This is using .NET core 3.1 modules and .NET 5.0 application (required in order for modules to work because microsoft 5.0 exe does not work with microsoft 5.0 dll).
The overloaded ToString method of IModule returns Handle which is a string that names the module, IE "ConsoleModule", and works as intended. Everything else is working except the data binding.
Can anyone else at least confirm this data binding method actually works in .NET 5.0 and/or 3.1? Rapidly losing sanity.
Whenever you have a sequence of similar items, that you want to show in a ComboBox, you need to tell the ComboBox which property of the items should be used to display each item. You were right, this is done using ComboBox.DisplayMember
Your Dictionary<IModule, AssemblyLoadContext> implements IEnumerable<KeyValuePair<IModule, AssemblyLoadContext>, so you can regard it as if it is a sequence of KeyValuePairs. Every KeyValuePair has a Key of type IModule, and a Value of type AssemblyLoadContext.
The IModule and the AssemblyLoadContext have several properties. You need to decide which property of them you want to show.
I am trying to bind the names of the IModules (IModule.Handle)
I guess that every IModule has a property Handle, and you want to display this Handle in the ComboBox.
comboBox1.DisplayMember = nameof(IModule.Handle);
If you need a display only, so no updates, it is enough to convert your original sequence into a list:
Dictionary<IModule, AssemblyLoadContext> myData = ...
comboBox.DataSource = myData.ToList();
However, if you want to update the displayed data, you need an object that implements IBindingList, like (surprise!) BindingList<T>. See BindingList.
You can make a BindingList<KeyValuePair<IModule, AssemblyLoadContext>>, but this is hard to read, hard to understand, difficult to unit test, difficult to reuse and maintain. My advice would be to create a special class for this.
I haven't got a clue what's in the IModule, so you'll have to find a proper class name. I'll stick with:
class DisplayedModule
{
public string DisplayText => this.Module.Handle;
public IModule Module {get; set;}
public AssemblyLoadContext AssemblyLoadContext{get; set;}
}
And in the constructor of your form:
public MyForm()
{
InitializeComponent();
this.ComboBox1.DisplayMember = nameof(DisplayedModule.DisplayText);
This way, if you want to change the text that needs to be displayed, all you have to do is change property DisplayText.
public BindingList<DisplayedModule> DisplayedItems
{
get => (BindingList<DisplayedModule>)this.comboBox1.DataSource;
set => this.comboBox1.DataSource = value;
}
You need procedures to get the initial data:
private Dictionary<IModule, AssemblyLoadContext> GetOriginalData() {...} // out of scope of this question
private IEnumerable<DisplayedModule> OriginalDataToDisplay =>
this.GetOriginalData().Select(keyValuePair => new DisplayedModule
{
Module = keyValuePair.Key,
AssemblyLoadcontext = keyValuePair.Value;
});
I have put this in separate procedures, to make it very flexible. Easy to understand, easy to unit test, easy to change and to maintain. If for instance your Original data is not in a Dictionary, but in a List, or an Array, or from a database, only one procedure needs to change.
To initially fill the comboBox is now a one-liner:
private ShowInitialComboBoxData()
{
this.DisplayedItems = new BindingList<DisplayedModule>
(this.OriginalDataToDisplay.ToList());
}
private void OnFormLoad(object sender, ...)
{
this.ShowInitialComboBoxData();
... // other inits during load form
}
If the operator adds / removed an element to the list, the bindinglist is automatically updated. If something happens, after which you know that the dictionary has been changed, you can simply change the bindingList For small lists that do not change often, I would make a complete new BindingList. If the List changes often, or it is a big list, consider to Add / Remove the original BindingList.
private void AddDisplayedModule(DisplayedModule module)
{
this.DisplayedItems.Add(module);
}
private void RemoveDisplayedMOdule(DisplayedModule module)
{
this.DisplayedItems.Remove(module);
}
private void ModuleAddedToDictionary(IModule module, AssemblyLoadContext assembly)
{
this.AddDisplayedModule(new DisplayedModule
{
Module = module,
AssemblyLoadContext = assembly,
})
}
If the operator makes some changes, and indicates he finished editing the comboBox, for instance by pressing the "Apply Now" button, you can simply get the edited data:
private void ButtonApplyNowClicked(object sender, ...)
{
// get the edited data from the combobox and convert to a Dictionary:
Dictionary<IModule, AssemblyLoadContext> editedData = this.DisplayedItems
.ToDictionary(displayedItem => displayedItem.Module, // Key
displayedItem => displayedItem.AssemblyLoadContext); // Value;
this.ProcesEditedData(editedData);
}
To access the Selected item of the comboBox
DisplayedModule SelectedModule => (DisplayedModule)this.comboBox1.SelectedItem;
Conclusion
By separating you data from the way that it is displayed, changes will be minimal if you decide to change your view: change Combobox into a ListBox, or even a DataGridView. Or if you decide to change your data: not a Dictionary, but a sequence from a Database
The Problem
Often controller classes (MVC, MVVM, MVP) that manipulate views have access to a multitude of services. The purpose of one of these services is to update the controller classes with data that is pertinent to the controller.
Take this class for example:
public sealed class RestaurantInformation
{
public IEnumerable<Networking.Models.ServerModels.Restaurant> NearestRestaurants { get; internal set; }
public IEnumerable<Networking.Models.ServerModels.Restaurant> NewestRestaurants { get; internal set; }
public IEnumerable<Networking.Models.ServerModels.Category> Categories { get; internal set; }
}
Whenever the service receives updated information from the network regarding Categories, NewestRestaurants or NearestRestaurants, it packages all of the data into an object which has the class type RestaurantInformation and sends it to the controller class to pass to the view.
I decided to put my C grade in my art GCSE to good use and construct diagrams to aid your understanding of my problem. (Apologies for what you are about to see.)
As you can now see, the flow is as follows:
The view loads which in turn calls the RestaurantViewControl.
The RestaurantViewControl then calls the RestaurantService to retrieve the new categories from the API.
The API returns the new categories to the RestaurantService. (You can see here the restaurant service now has a list that contains B).
The RestaurantService then notify's the RestaurantViewControl using the class above with the new list of categories!
We must now update the list of categories in the RestaurantViewControl in the most efficient way possible with the new items.
I am currently clearing the categories list and then replacing all the values with the new list. The two things I want to know are:
What is the most efficient way to detect a change in the Categories List object?
What is the most efficient way to update the categories list which may still contain objects that are completely valid in that list.
Seems like you have a straight forward issue. You will have a services layer that calls when you show the restaurant list page.
So your collectionView/listView just displays the list of items in the view cell based on that data. One example https://almirvuk.blogspot.com/2019/07/lets-play-with-collectionview-layouts.html?m=1
Usually you’ll just do a check for changes on the first time you visit the page, pull to refresh, or if you set up caching-after a set time when the cache expires.
I have a WPF application that displays a list of files/folders to the user in a treeview and they select which ones they want to load for the app. The ViewModel stores the folders/files in a DirectoryItem class that is binded to the UI:
public class DirectoryItem : INotifyPropertyChanged
{
public String DisplayName { get; set; }
public String Fullpath { get; set; }
public ObservableCollection<DirectoryItem> Children { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
}
The way I'm handling it now, the Model is passed a root directory and returns a List of string with every file in/under the root directory. Then, the ViewModel has a BuildDirectoryItemCollection function that converts the list into the ObservableCollection of DirectoryItems.
This is my first WPF app so I want to make sure I'm following the MVVM pattern correctly. Am I correct in thinking that the conversion from the List to ObservableCollection should take place in the ViewModel? Should the Model even have knowledge of the DirectoryItem class?
And as a side note, I'm not convinced that passing in a List of filepaths and then rebuilding the folder structure from that List is a good way of building my ObservableCollection. I am open to suggestions on a better way to do this!
Thanks for your time.
It's a bit unclear what your model actually is in this case, but since you are just creating DirectoryItem objects based on the structure of a folder on disk, you might as well do all this, i.e. build the ObservableCollection<DirectoryItem>, directly in the view model class.
So you could for example inject the view model class with the path of the root directory (a string) and create the ObservableCollection<DirectoryItem> that you bind to in the view directly in the constructor of the view model.
It seems unnecessary to first build a List<string> and then build an ObservableCollection<DirectoryItem> based on this list instead of building the ObservableCollection<DirectoryItem> immediately.
A method that creates a hierarchical collection based on a file path is perfectly fine to implement in the view model. Don't overcomplicate things :)
I'm new at WPF and I'm trying to use MVVM. I'm using CollectionViewSource in my view-model against a SQL database using Entity Framework.
So let's say I have this class:
public class People
{
public int Id { get; set; }
public string name { get; set; }
}
And lets say this is what I have in the database:
ID: Name:
Bugs Bunny
Mick Jagger
Mickey Mouse
Donald Duck
Goofy
Elmer Fudd
Daffy Duck
Porky Pig
Now using CollectionViewSource.View, I know how to use the methods MoveCurrentToNext(), MoveCurrentToPrevious(), etc. and that works fine, but I need to move to a specific name, for example Donald Duck. So if I do this:
dbContext.Peoples.Find(4);
Or if I do this
dbContext.Peoples.Where(p => p.Name == "Donald Duck").FirstOrDefault();
That will get me the right object from the database using Entity Framework. However, if I do this:
CollectionViewSource.View.MoveCurrentTo(dbContext.Peoples.Find(4));
It will not move, and CollectionViewSource.View.CurrentItem is null.
So then how would someone move to the correct item?
I guess this is because the reference that you get when calling dbContext.Peoples.Find(4) is not the same as the one you have in your CollectionView source collection.
The CollectionViewSource.View.MoveCurrentTo (an others 'MoveTo' methods of the collectionView) requires an argument that is the same reference as the one in your source collection.
Then, if your dbContext methods to retreive an object returns a new instance or an instance different than the one in your CollectionView, this won't work.
So either use an intermediate collection as the source of the collection view to keep a unique reference to the objects (and update these references when the object in in data access context change) or try to implement equality members in your People class (never tried the later solution but should work).
Also, try to add in your question some complete piece of code rather than code fragments about which we can't realy see exactly where is the problem.
I have a problem which I cant seem to find answer to through searches (either that or I am searching for the completely wrong thing!). I have a list of items called "Top 10" in a sortedlist item that is populated from my DB (SortedList where int is position and string is item). I want to be able to move items up & down the list order at the click of a button and then save the new order back to the DB. I am OK with the DB part it is just the re-ordering I am really struggling with - is a sortedlist the correct collection for this?
Many thanks for any advice offered.
A SortedList is for maintaining order within your SortedList as you add or remove items from it.
You should create a custom list of your objects and then sort on property of that object.
So if your entry in your database was like this you could place it in an object, add it to a list and then sort it using Lambda on which ever criteria you like
public class LeaguePosition
{
public int Position { get; set; }
public string Team { get; set; }
}
List<LeaguePosition> League = new List<LeaguePosition>();
League.Add(new LeaguePosition() { Position = 2, Team = "Wolves" });
League.Add(new LeaguePosition() { Position = 3, Team = "Spurs" });
League.Add(new LeaguePosition() { Position = 1, Team = "Villa" });
League.Sort((teamA, teamB) => teamA.Position.CompareTo(teamB.Position));
You can also then use RemoveAt() and Insert() to move items about to custom positions within the list.
LeaguePosition teamToMove = League[1];
League.RemoveAt(1);
League.Insert(2, teamToMove);
No, a SortedList will keep things in sorted (alpha/numeric) order. You want a plain list that will let you pull things out and insert them at different positions.
A SortedList is going to force each item to keep track of its position within the list relative to the other items (your int position). This should be a responsiblity of the Collection, so you should use some sort of Collection that will allow you to move things up/down/around without having to manually correct each item.
I would say a SortedList is exactly what you want. When you get data back from the database you want to keep it in order based on the position field (what I can tell from your OP). I'm assuming you will have a class that contains the data you're getting back from the DB. To keep the data in the right order you will need to implement the IComparable interface in your custom class so that the SortedList knows what values to use to keep the list in order. This way as you add/remove items you will keep them in the right order.
You could use a generic List<> but then you have to write all the code to sort your items yourself, the SortedList already does this so why re-invent the wheel?
Take a look at this page for more info:
http://www.dotnetspider.com/resources/4679-Applying-custom-sort-SortedList.aspx