276 lines
8.7 KiB
Markdown
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
|