I am using OxyPlot to draw some diagrams in my .NET WPF project. MVVM is a new thing to me, so i am not sure how to follow it in this situation.
- There is a window (main view) that contains PlotView.
- There is a model that contains a collection of objects. Each object corresponds to a graph on my plot.
- There is a view model which creates
LineSeries
, puts it intoPlotModel
.
The problem is that i want each graph to be clickable. After a click another window opens with more detailed information on the corresponding object. My current view model is something like this:
public class MainViewModel : BaseViewModel
{
public PlotModel MyPlotModel { get; set; }
private MyModel model;
public MainViewModel()
{
model = new MyModel();
DrawGraphs();
}
private void DrawGraphs()
{
List<MyObject> myObjects = model.GetObjects();
foreach (var obj in myObjects)
{
var s = new LineSeries();
// Setting up datapoints and appearance of the graph
s.MouseDown += (object sender, OxyMouseDownEventArgs e) =>
{
var view = new ObjectView(); // A view with full information of this specific object
view.DataContext = new ObjectViewModel(obj); // View model uses the object as a parameter to do bindings for new view
view.ShowDialog();
};
MyPlotModel.Series.Add(s);
}
OnPropertyChanged("MyPlotModel");
}
}
MouseDown can be set up only on series creation because i need to know a specific object from my model that corresponds to this graph.
It works fine to me, however, i doubt that implementation follows MVVM. View model should not be able to create new views. But i cannot come up with a better solution. Is there a more elegant and MVVM-ish way to do this?
4
With a few caveats, this is a more MVVM-friendly approach (I say more because it may not be feasible to do a truly pure MVVM approach to the extent I understand this library). In short, you would move some of the view-opening functionality into a view-level class to manage your views and bind it to an ObservableCollection
on the view model, to which you can add ObjectViewModel
instances that will trigger the creation of new views:
Static ObjectViewManager Class with Attached Property
public static class ObjectViewManager
{
public static readonly DependencyProperty ViewsProperty =
DependencyProperty.RegisterAttached(
"Views",
typeof(INotifyCollectionChanged),
typeof(ObjectViewManager),
new PropertyMetadata(OnViewsChanged));
public static INotifyCollectionChanged GetViews(DependencyObject obj)
{
return (INotifyCollectionChanged)obj.GetValue(ViewsProperty);
}
public static void SetViews(DependencyObject obj, INotifyCollectionChanged value)
{
obj.SetValue(ViewsProperty, value);
}
private static void OnViewsChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
if (e.OldValue is INotifyCollectionChanged oldIncc)
oldIncc.CollectionChanged -= OnViewsCollectionChanged;
if (e.NewValue is INotifyCollectionChanged newIncc)
newIncc.CollectionChanged += OnViewsCollectionChanged;
}
private static void OnViewsCollectionChanged(
object? sender,
NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
if (e.NewItems == null)
return;
foreach (var item in e.NewItems)
{
var view = new ObjectView(); // A view with full information of this specific object
view.DataContext = item; // View model uses the object as a parameter to do bindings for new view
view.ShowDialog();
if (sender is IList list)
{
// Optionally remove the view model
// from the collection when the dialog
// closes so view model and view remain
// in sync
list.Remove(item);
}
}
break;
}
}
}
Changes to View Model
public class MainViewModel : BaseViewModel
{
// ...
ObservableCollection<ObjectViewModel> _ObjectViewModels;
public ObservableCollection<ObjectViewModel> ObjectViewModels
{
get
{
return _ObjectViewModels ??
(_ObjectViewModels =
new ObservableCollection<ObjectViewModel>());
}
}
private void DrawGraphs()
{
List<MyObject> myObjects = model.GetObjects();
foreach (var obj in myObjects)
{
var s = new LineSeries();
// *** More about this event handler below ***
s.MouseDown += (object sender, OxyMouseDownEventArgs e) =>
{
// Rather than create `ObjectView` here,
// adding it to the ObservableCollection
// will trigger invocation of the collection change
// handler above, provided the ObjectViewModels
// property is bound somewhere in XAML
this.ObjectViewModels.Add(new ObjectViewModel(obj));
};
MyPlotModel.Series.Add(s);
}
OnPropertyChanged("MyPlotModel");
}
}
Somewhere in XAML, on some root view or even MainWindow
<SomeRootView local:ObjectViewManager.Views="{Binding ObjectViewModels}" />
That’s more or less it, but now for a pretty big chunk of salt:
-
The
MouseDown
handler in the view model is an MVVM no-no, albeit a qualified one in this case. It’s qualified because as far as I can tell (though I’m not familiar with the library)LineSeries
doesn’t look like it inherits from a WPF type. So they’ve already given you, it seems, an abstraction layer between the underlying UI and your application logic, which one could argue makes it ok for the view model because it doesn’t tether you to one UI platform. However, purists would tell you that’s still no excuse, because the very concept ofMouseDown
is too much in the UI implementation detail camp to be worthy of view model treatment. -
The problem then is where to put it? It just may not be possible to put it anywhere else. Moreover, the library and any code that relies on it is already platform-independent. If you changed it around to create the
LineSeries
in the view layer, just so you can handle theMouseDown
event in the “correct” place, then all you’re doing is making more of your code WPF platform dependent. Why bother?
OxyPlot is clearly a highly cross-platform library. These can be tricky to work with in a pure MVVM fashion unless specifically designed to be MVVM friendly (e.g., MAUI) because many times they’ve already done cross-platform in their own style, and making them MVVM friendly would just tether them more closely to WPF, defeating the very purpose.
If that’s the case, then I say don’t fight the current. By all means pull in MVVM techniques to the extent they help you maintain cross-platform compatibility (which my example hopefully does), but otherwise just embrace their flavor of doing things. After all, there’s no reason to use any design pattern unless it brings more benefit than cost.