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

8.7 KiB

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

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

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

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

<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

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

[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

[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

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

<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