Files
DP44/GLM5Analysis/PatternLibrary/MVVM_Pattern.md
2026-04-17 14:55:32 -04:00

276 lines
8.7 KiB
Markdown

# MVVM Pattern
## When to Use
- All UI development in DataPRO
- Creating views that need to be testable
- Separating business logic from presentation
## Files to Create/Modify
### Standard Structure
```
{Module}/
├── Interface/
│ ├── I{ViewModelName}.cs (in DTS.Common or DTS.CommonCore)
│ └── I{ViewName}.cs (in DTS.Common or DTS.CommonCore)
├── View/
│ ├── {ViewName}.xaml
│ └── {ViewName}.xaml.cs
└── ViewModel/
└── {ViewModelName}.cs
```
## Code Templates
### 1. View Interface
**File:** `DTS.CommonCore/Interface/{Module}/I{ViewName}.cs`
```csharp
using DTS.Common.Base;
namespace DTS.Common.Interface.{Module}
{
public interface I{ViewName} : IBaseView
{
// Add view-specific properties/methods if needed
}
}
```
### 2. ViewModel Interface
**File:** `DTS.CommonCore/Interface/{Module}/I{ViewModelName}.cs`
```csharp
using DTS.Common.Base;
namespace DTS.Common.Interface.{Module}
{
public interface I{ViewModelName} : IBaseViewModel
{
I{ViewName} View { get; set; }
// Add other properties/methods
}
}
```
### 3. ViewModel Implementation
```csharp
using System;
using System.ComponentModel;
using System.ComponentModel.Composition;
using Prism.Events;
using Prism.Regions;
using Unity;
using DTS.Common.Interface.{Module};
using DTS.Common.Interactivity;
using DTS.Common.Events;
namespace {ModuleName}
{
[PartCreationPolicy(CreationPolicy.Shared)]
public class {ViewModelName} : I{ViewModelName}
{
public I{ViewName} View { get; set; }
private IEventAggregator _eventAggregator { get; }
private readonly IRegionManager _regionManager;
private IUnityContainer UnityContainer { get; }
public InteractionRequest<Notification> NotificationRequest { get; }
public InteractionRequest<Confirmation> ConfirmationRequest { get; }
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public {ViewModelName}(
I{ViewName} view,
IRegionManager regionManager,
IEventAggregator eventAggregator,
IUnityContainer unityContainer)
{
View = view;
View.DataContext = this;
NotificationRequest = new InteractionRequest<Notification>();
ConfirmationRequest = new InteractionRequest<Confirmation>();
_eventAggregator = eventAggregator;
UnityContainer = unityContainer;
_regionManager = regionManager;
_eventAggregator.GetEvent<RaiseNotification>().Subscribe(OnRaiseNotification);
_eventAggregator.GetEvent<BusyIndicatorChangeNotification>()
.Subscribe(OnBusyIndicatorNotification, ThreadOption.PublisherThread, true);
}
private bool _isBusy = false;
public bool IsBusy
{
get => _isBusy;
set { _isBusy = value; OnPropertyChanged("IsBusy"); }
}
public bool IsDirty { get; private set; }
public bool IsMenuIncluded { get; set; }
public bool IsNavigationIncluded { get; set; }
public void Initialize() { }
public void Initialize(object parameter) { }
public Task InitializeAsync() => Task.CompletedTask;
public Task InitializeAsync(object parameter) => Task.CompletedTask;
public void Activated() { }
public void Cleanup() { }
public Task CleanupAsync() => Task.CompletedTask;
private void OnBusyIndicatorNotification(bool eventArg) => IsBusy = eventArg;
private void OnRaiseNotification(NotificationContentEventArgs eventArgs)
{
NotificationRequest.Raise(new Notification {
Content = eventArgs,
Title = eventArgs.Title
});
}
}
}
```
### 4. XAML View
```xml
<base:BaseView x:Class="{ModuleName}.{ViewName}"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:base="clr-namespace:DTS.Common.Base;assembly=DTS.Common"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:converters="clr-namespace:DTS.Common.Converters;assembly=DTS.Common">
<base:BaseView.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/DTS.Common;component/Themes/CommonStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<converters:BooleanToVisibilityConverter x:Key="BoolToVisibility" />
</ResourceDictionary>
</base:BaseView.Resources>
<Grid>
<!-- Controls bound to ViewModel properties -->
<ListView ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem" BasedOn="{StaticResource TTS_ListViewItemStyle}">
<Style.Triggers>
<DataTrigger Binding="{Binding Status}" Value="Error">
<Setter Property="Background" Value="{StaticResource Brush_ApplicationStatus_Failed}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListView.ItemContainerStyle>
</ListView>
</Grid>
</base:BaseView>
```
### 5. View Code-Behind
```csharp
using System.Windows.Controls;
using DTS.Common.Base;
using DTS.Common.Interface.{Module};
namespace {ModuleName}
{
public partial class {ViewName} : BaseView, I{ViewName}
{
public {ViewName}()
{
InitializeComponent();
}
}
}
```
## Examples from Codebase
### Example 1: SensorsListViewModel
**File:** `DataPRO/Modules/SensorsList/SensorsList/ViewModel/SensorsListViewModel.cs:41`
```csharp
[PartCreationPolicy(CreationPolicy.Shared)]
public class SensorsListViewModel : ISensorsListViewModel
{
public ISensorsListView View { get; set; }
private IEventAggregator _eventAggregator { get; }
private readonly IRegionManager _regionManager;
private IUnityContainer UnityContainer { get; }
public SensorsListViewModel(
ISensorsListView view,
IRegionManager regionManager,
IEventAggregator eventAggregator,
IUnityContainer unityContainer)
{
View = view;
View.DataContext = this;
_eventAggregator = eventAggregator;
// ...
}
}
```
### Example 2: ShellViewModel (BindableBase)
**File:** `DataPRO/DataPRO/ViewModel/ShellViewModel.cs:26`
```csharp
[Export(typeof(IShellView))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class ShellViewModel : BindableBase, IShellViewModel
{
public IShellView View { get; private set; }
public ShellViewModel(
IShellView view,
IRegionManager regionManager,
IEventAggregator eventAggregator,
IUnityContainer unityContainer)
{
View = view;
View.DataContext = this;
// ...
}
}
```
### Example 3: ISensorsListViewModel Interface
**File:** `Common/DTS.CommonCore/Interface/Sensors/SensorsList/ISensorsListViewModel.cs:8`
```csharp
public interface ISensorsListViewModel : IBaseViewModel, IFilterableListView
{
ISensorsListView View { get; set; }
IAnalogSensor[] AnalogSensors { get; set; }
void GetSensors(int sensorCalWarningPeriodDays, bool included);
void Filter(string currentFilter);
}
```
### Example 4: XAML Binding Pattern
**File:** `DataPRO/Modules/SensorsList/SensorsList/View/SensorsListView.xaml:44`
```xml
<ListView ItemsSource="{Binding AnalogSensors, UpdateSourceTrigger=PropertyChanged}"
SelectedIndex="{Binding SelectedAnalogIndex, Mode=TwoWay}">
<i:Interaction.Behaviors>
<behaviors:MultiSelectionBehavior SelectedItems="{Binding SelectedAnalogItems}" />
</i:Interaction.Behaviors>
</ListView>
```
## Common Mistakes to Avoid
1. **Not setting DataContext in constructor** - View won't bind to ViewModel
2. **Forgetting INotifyPropertyChanged** - UI won't update when properties change
3. **Using concrete types instead of interfaces** - Breaks DI and testability
4. **Putting logic in code-behind** - Should be in ViewModel
5. **Not using InteractionRequest for dialogs** - Use `NotificationRequest.Raise()` instead of `MessageBox.Show()`
6. **Hardcoded strings in OnPropertyChanged** - Use `nameof()` when possible
7. **Not implementing IBaseViewModel fully** - Missing Initialize/Cleanup methods
8. **Blocking UI thread** - Use async/await for long operations