I am trying to determine the best method to populate a region when the selected item of a data grid view changes.
I am using Prism.DryIoc in my WPF desktop application. It has several regions, each with corresponding loosely coupled views and viewmodels. When the selected item of this data grid changes, I want a different view to populate a different region.
These views do not have references to each other even though they are contained in the same module project file.
So far, I have tried using the IEventAggregator to publish that the selected item has changed.
public class MyObjectViewModel : BindableBase
{
private IEventAggregator _ea;
public MyObjectViewModel(IEventAggregator ea )
{
_ea = ea;
//Do stuff here …
Grid_SelectionChanged = new DelegateCommand(SelectObject, CanSelectObject);
}
private MyData _selectedObject;
public MyData SelectedObject
{
get { return _selectedObject; }
set
{
SetProperty(ref _selectedObject, value);
_ea.GetEvent<MyObjectSelectionChangedEvent>().Publish(SelectedObject);
}
} …
In my MyAppModule.cs file I have the following constructor where I subscribe to the event:
public MyAppModule(IRegionManager regionManager, IEventAggregator ea)
{
_regionManager = regionManager;
ea.GetEvent<MyObjectSelectionChangedEvent>().Subscribe(MyObjectSelectionChanged);
}
I have a corresponding method that reads a property from SelectedObject.ObjectName.
private void MyObjectSelectionChanged(MyObject myObject)
{
Type myObjectType = null;
switch (myObject.ObjectName)
{
case "objectA":
myObjectType = typeof(Views.MyObjectA);
break;
case "objectB":
myObjectType = typeof(Views.MyObjectB);
break;
...
default:
myObjectType = typeof(Views.NoObjects);
break;
}
_regionManager.AddToRegion("TestDataRegion", deviceType);
}
When I debug this, the publish event is called but the above method is not. The subscribe method is called when the object is created.
Am I pursuing the correct solution in order to dynamically load a view based on a selected item in the datagrid object?
What am I missing to get my method called after publishing that the event changed?
Is there a better way to handle this in Prism so that I don't have to have a dirty setter?
EDIT: I have switched to use RegisterForNavigation and RequestNavigation. Thank you for the tip
Related
I have a combo box that is bound to an object from a model that is instantiated inside of my view model. OnPropertyChange is handled inside of the Notifier class that inherits from INotifyPropertyChange. The view model polls executes a method from a data access layer and returns an observablelist to the view model. This is then passed into a constructor that builds the object i want to bind to the combo box. The object has two properties. 1) An observable list of possible selections and 2) a string that represents the current selected item.
Here is the problem. The combo box is successfully bound and populated by the list. It does not however appear to call the setter method when an item is changed. I need this functionality so I can continue with application logic once the item is elected. Debugging confirms that no setter is called, only the get. The setter is in fact called on the model however which make sense. Im certain I am missing something here and am open to suggestions on a better way to do this.
Model
public class WellListGroup : Notifier
{
private ObservableCollection<string> _headers;
public ObservableCollection<string> headers
{
get { return _headers; }
set { _headers = value; OnPropertyChanged("headers"); }
}
private string _selected;
public string selected
{
get { return this._selected;}
set { this._selected = value; OnPropertyChanged("selected");}
}
}
Notifier
public class Notifier : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
View Model
public class MainViewModel : Notifier
{
//data access layer
public static getWells gw = new getWells();
//set combo box
public static ObservableCollection<string> headers = gw.getHeaders();
private WellListGroup _wlg = new WellListGroup {headers = headers, selected = headers[0]};
public WellListGroup wlg
{
get {
return _wlg;
}
set {
_wlg = value;
OnPropertyChanged("wlg");
OnChange()// do stuff!!!
}
}
View
<ComboBox x:Name="groupComboBox"
DockPanel.Dock="Top"
ItemsSource = "{Binding Path = wlg.headers}"
SelectedItem="{Binding Path = wlg.selected, Mode=TwoWay}">
</ComboBox>
EDIT - Reworked ViewModel to Subscribe to event on the object
public class MainViewModel : Notifier
{
//data access layer
public static getWells gw = new getWells();
//set combo box
public static List<string> headers = gw.getHeaders();
private WellListGroup _wlg = new WellListGroup {headers = headers, selected = headers[0]};
public WellListGroup wlg
{
get {
return _wlg;
}
set {
_wlg = value;
OnPropertyChanged("wlg");
OnChange(_wlg.selected);// do stuff!!!
}
}
public MainViewModel()
{
// Move this into the constructor to avoid any race conditions
_wlg = new WellListGroup {headers = headers, selected = headers[0]};
// Subscribe to the property change even for WLG
_wlg.PropertyChanged += (sender, args) =>
{
if (args.PropertyName == "selected") {
}
OnChange(_wlg.selected);// do stuff!!!
};
}
The situation is that the reflected property setter is within the _wlg class and not the setter of the _wlg class itself on the VM. The bounded item is not going to the top level but the lower property as mentioned.
Either put in a commanding system to kick off the OnChange()// do stuff!!! code or subscribe to the _wlg class instance INotifyProptertyChanged event and call the method you mentioned.
Is there anyway to handle either of those from within the view model?
Yes, subscribe to the instance of the class WellListGroup property changed event and look for selected or others to report a change.
public MainViewModel()
{
// Move this into the constructor to avoid any race conditions
_wlg = new WellListGroup {headers = headers, selected = headers[0]};
// Subscribe to the property change even for WLG
_wlg.PropertyChanged += (sender, args) =>
{
if (args.PropertyName == 'selected')
OnChange()// do stuff!!!
};
}
Of note, it is unclear if you really need to hold the strings in an ObservableCollection. That collection has its own implementation of notify events for adding and deleting of items within the collection.
If the VM needs that specific change info, then you will need to subscribe to the ObservableCollection's event(s) for such operations instead of/as well as the aforementioned above example.
If one does not need those notifications, holding the strings in an ObservableCollection is not needed and you can change it to a List<string> instead.
Edit:
Turns out I was merging a list with the latest data from a rest API with the data in my GUI. All I really had to do was clear and refill my observablecollection. This post is basically an xy problem. I did not have the vocabulary to explain the problem I was facing.
I'm building an app with a Data class where I store all my data. In this class I have a List filled with objects. I have a Page with a ObservableCollection and a ListView. Currently when I update the ObservableCollection, I clear it and refill it with all the data from Data-class's List. When I do this, my ListView flickers. My guess is that completely rebuilding the ObservableCollection causes this, in combination with a custom ViewCell that is not the lightests. How could I go about updating only what I want? The list/o.collection can have diffrent sizes. The list/o.collection both store the same object.
What I tried:
List<csharp.Truck> favs = data.getFavoriteTrucks();
trucks.Clear();
foreach (csharp.Truck truck in favs)
{
trucks.Add(truck);
}
}
Works but makes my ListView flicker.
Trying this now, its pretty bad code I think, it does update the list how I want it to but the listview does not get updated for some reason. Maybe I need to trigger a refresh?
List<csharp.Truck> all = data.getTrucks();
//if list sizes are not equal(excess/missing objects)
if (all.Count != trucks.Count)
{
//excess object
if (all.Count < trucks.Count)
{
trucks.Clear();
foreach(csharp.Truck t in all)
{
trucks.Add(t);
}
}
//object missing
if(all.Count > trucks.Count)
{
foreach(csharp.Truck t in all)
{
if (!trucks.Contains(t))
{
trucks.Add(t);
}
}
}
}//objects are now the same
//test if object is present but outdated(favorite property)
foreach(csharp.Truck t in trucks)
{
if (t.isFavorite() != all[all.IndexOf(t)].isFavorite())
{
t.setFavorite(all[all.IndexOf(t)].isFavorite());
}
}
Also let me know if this approach is not good practise in the first place.
If you want to update only some properties, you could implement in your model class INotifyPropertyChanged interface.
public class Model : INotifyPropertyChanged
{
private string _myProperty;
public string MyProperty
{
get { return _myProperty; }
set
{
_myProperty = value;
RaisePropertyChanged();
}
}
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged([CallerMemberName]string propertyName = "")
{
Volatile.Read(ref PropertyChanged)?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
And then if this property is binded to view, view will know, when you change it:
<Label Text="{Binding MyProperty}" />
I'm working on implementing a master/details view in my application using a TreeView and a custom details view control. I'm also trying to stick to the MVVM pattern.
Right now the TreeView is bound to a collection of view model objects that contain all of the details and the details view is bound to the selected item of the TreeView.
This works great... until one of the TreeView nodes has 5,000 children and the application is suddenly taking up 500MB of RAM.
Main window view model:
public class MainWindowViewModel
{
private readonly List<ItemViewModel> rootItems;
public List<ItemViewModel> RootItems { get { return rootItems; } } // TreeView is bound to this property.
public MainWindowViewModel()
{
rootItems = GetRootItems();
}
// ...
}
Item view model:
public ItemViewModel
{
private readonly ModelItem item; // Has a TON of properties
private readonly List<ItemViewModel> children;
public List<ItemViewModel> Children { get { return children; } }
// ...
}
Here's how I'm binding the details view:
<View:ItemDetails DataContext="{Binding SelectedItem, ElementName=ItemTreeView}" />
I'm fairly new to WPF and the MVVM pattern, but it seems like a waste to I want to bind the TreeView to a collection of a smaller, simplified object that only has properties necessary for displaying the item (like Name and ID), then once it is selected have all of the details loaded. How would I go about doing something like this?
Overview
This should be a simple matter of binding the TreeView's selected item property to something on your source. However, because of the way the TreeView control was built, you have to write more code to get an MVVM-friendly solution, using out-of-the-box WPF.
If you're using vanilla WPF (which I'm assuming you are), then I'd recommend going with an attached behavior. The attached behavior would bind to an action on your main view model that would be invoked when the TreeView's selection changes. You could also invoke a command instead of an action, but I'm going to show you how to use an action.
Basically, the overall idea is to use one instance of your details view model that will be made available as a property of your master view model. Then, instead of your RootItems collection having hundreds of instances of view models, you can use light-weight objects that simply have a display name for the node and perhaps some kind of id field behind them. When the selection on your TreeView changes, you want to notify your details view model by either calling a method or setting a property. In the demonstration code below, I'm setting a property on the DetailsViewModel called Selection.
Walkthrough with Code
Here's the code for the attached behavior:
public static class TreeViewBehavior
{
public static readonly DependencyProperty SelectionChangedActionProperty =
DependencyProperty.RegisterAttached("SelectionChangedAction", typeof (Action<object>), typeof (TreeViewBehavior), new PropertyMetadata(default(Action), OnSelectionChangedActionChanged));
private static void OnSelectionChangedActionChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var treeView = sender as TreeView;
if (treeView == null) return;
var action = GetSelectionChangedAction(treeView);
if (action != null)
{
// Remove the next line if you don't want to invoke immediately.
InvokeSelectionChangedAction(treeView);
treeView.SelectedItemChanged += TreeViewOnSelectedItemChanged;
}
else
{
treeView.SelectedItemChanged -= TreeViewOnSelectedItemChanged;
}
}
private static void TreeViewOnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var treeView = sender as TreeView;
if (treeView == null) return;
InvokeSelectionChangedAction(treeView);
}
private static void InvokeSelectionChangedAction(TreeView treeView)
{
var action = GetSelectionChangedAction(treeView);
if (action == null) return;
var selectedItem = treeView.GetValue(TreeView.SelectedItemProperty);
action(selectedItem);
}
public static void SetSelectionChangedAction(TreeView treeView, Action<object> value)
{
treeView.SetValue(SelectionChangedActionProperty, value);
}
public static Action<object> GetSelectionChangedAction(TreeView treeView)
{
return (Action<object>) treeView.GetValue(SelectionChangedActionProperty);
}
}
Then, in the XAML on your TreeView element, apply the following: local:TreeViewBehavior.SelectionChangedAction="{Binding Path=SelectionChangedAction}". Note that you will have to substitute local for the namespace of the TreeViewBehavior class.
Now, add the following properties to your MainWindowViewModel:
public Action<object> SelectionChangedAction { get; private set; }
public DetailsViewModel DetailsViewModel { get; private set; }
In your MainWindowViewModel's constructor, you need to set the SelectionChangedAction property to something. You might do SelectionChangedAction = item => DetailsViewModel.Selection = item; if your DetailsViewModel has a Selection property on it. That's entirely up to you.
And finally, in your XAML, wire the details view up to its view model like so:
<View:ItemDetails DataContext="{Binding Path=DetailsViewModel}" />
That's the basic architecture of an MVVM friendly solution using straight WPF. Now, with that said, if you're using a framework like Caliburn.Micro or PRISM, your approach would probably be different than what I've provided here. Just keep that in mind.
I have a strange behavior going on. I'm using MVVM pattern, i have a binding to an Observable collection named AlarmCollection to a grid control in a View named AlarmView. When i create multiple instances of a AlarmModelView class, and add items to AlarmCollection, all the instances display the changes.
Any changes to the ObservableColelction AlarmCollection, affects all the bound ItemSources of the grid controls.
I have tried to lock the dispatcher thread, from a similar post here, to no avail.
Is there anyway to keep the changes to this Observable collection, within each instance of the ViewModel? So that each modification does not affect any other collection in the UI thread.
Any help is appreciated.
[edit below]
It is strange scenario, I need to zoom/drill into what is rendered by creating the new instances of the Child MV, which in turn adds tabs to the Parent MV. The Child Views are all bound to the same Collection names, and all are being updated by a WCF Async call. I need X number multiple instances, based on the how deep the zoom level goes, so i need 1 ModelView object.
How would i achieve this using CollectionChanged event or creating the ModelView's own CollectionView?
private MainViewModel _parentViewModel;
public MainViewModel ParentViewModel
{
get { return _parentViewModel; }
set
{
if (ParentViewModel == value) { return; }
SetPropertyValue(ref _parentCircuitViewModel, value, "ParentViewModel");
}
}
private ObservableCollection<DetailEntity> _alarmCollection;
public ObservableCollection<DetailEntity> AlarmCollection
{
get
{
if (_alarmCollection == null)
_alarmCollection = new ObservableCollection<DetailEntity>();
return _alarmCollection;
}
private set { _alarmCollection = value; }
}
ServiceNode _selectedNode;
public ServiceNode SelectedNode
{
get { return _selectedNode; }
set
{
SetPropertyValue(ref _selectedNode, value, "SelectedNode");
// render selected child node service path
RenderSubPath(_selectedNode);
// reset storage value
_selectedCircuitNode = null;
}
}
// Constructor
public RenderViewModel(string servicePath CircuitMainViewModel parentViewModel)
{
ServicePath = servicePath,
ParentCircuitViewModel = parentViewModel;
// event to handler for completed async calls
Client.GetAlarmsByNodeListCompleted += new EventHandler<GetAlarmsByNodeListCompletedEventArgs>(Client_GetAlarmsByNodeListCompleted);
}
void RenderSubPath(ServiceNode childNode)
{
if (childNode == null)
return;
// create a new child instance and add to parent VM tab
_parentViewModel.AddServiceRenderTab(new ViewModel.Workspaces.RenderViewModel(childNode.elementPath _parentViewModel);
}
// wcf async webservice call to add alarm to ObservableCollection
// ** This is updating all Collections in all Views.
void Client_GetAlarmsByNodeListCompleted(object sender, AlarmServiceReference.GetAlarmsByNodeListCompletedEventArgs e)
{
try
{
if (e.Result == null)
return;
// add to parent Netcool alarm collection
foreach (DetailEntity alarm in nodeAlarms)
{
_alarmCollection.Add(alarm);
}
}
}
From your description, it sounds as though all your views are bound to the same underlying collection. For any collection you bind to, WPF will actually bind to a collection view (ICollectionView) wrapped around that collection. If you don't explicitly create your own collection view, it will use a default one. Any binding to the same collection will result in the same collection view being used.
It's hard to say without seeing your code, but it's likely you want to either use a separate instance of the underlying view model (and, hence, the collection) or you want to explicitly create separate collection views and bind to them instead.
[EDIT] To be clear, I know how to get a list of forms via reflection. I'm more concerned with the design time property grid.
I have a user control with a public property of the type Form.
I want to be able to select a form at design time from a dropdown.
I want to populate the form dropdown list from a set namespace: UI.Foo.Forms
This would work like if you have a public property of Control. At design time, the property will automatically populate a dropdown with all the controls on the form, for you to select from. I just want to populate it with all the forms in a namespace.
How do I go about doing this? I hope I'm being clear enough so there is no confusion. I'm looking for some code examples if at all possible. I'm trying to avoid having to spend too much time on this when I have other deadlines to meet.
Thanks for your help in advance.
You can easily get the classes via Reflection:
var formNames = this.GetType().Assembly.GetTypes().Where(x => x.Namespace == "UI.Foo.Forms").Select(x => x.Name);
Assuming you're calling this from code in the same assembly as your forms, you'll get the names of all the types that are in the "UI.Foo.Forms" namespace. You can then present this in the dropdown and, eventually, instantiate whichever is selected by the user via reflection once more:
Activator.CreateInstance(this.GetType("UI.Form.Forms.FormClassName"));
[Edit] Adding code for the design time stuff:
On your control you can create a Form property as such:
[Browsable(true)]
[Editor(typeof(TestDesignProperty), typeof(UITypeEditor))]
[DefaultValue(null)]
public Type FormType { get; set; }
Which references the Editor type that must be defined. The code is pretty self-explanatory, with a minimal amount of tweaking, you'll likely be able to get it to produce exactly what you want.
public class TestDesignProperty : UITypeEditor
{
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.DropDown;
}
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
{
var edSvc = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
ListBox lb = new ListBox();
foreach(var type in this.GetType().Assembly.GetTypes())
{
lb.Items.Add(type);
}
if (value != null)
{
lb.SelectedItem = value;
}
edSvc.DropDownControl(lb);
value = (Type)lb.SelectedItem;
return value;
}
}
The dropdown does not close when item gets selected by clicking it, so this could be useful:
assign the click event handler for the listbox and add the event handler function
public class TestDesignProperty : UITypeEditor
{
// ...
IWindowsFormsEditorService editorService;
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
{
// ...
editorService = edSvc ; // so can be referenced in the click event handler
ListBox lb = new ListBox();
lb.Click += new EventHandler(lb_Click);
// ...
}
void lb_Click(object sender, EventArgs e)
{
editorService.CloseDropDown();
}
}