Files
DP44/GLM5Analysis/PromptTemplates/FixBugInViewModel.md
2026-04-17 14:55:32 -04:00

12 KiB

Fix Bug in ViewModel - DataPRO Prompt Template

Context

DataPRO uses the MVVM (Model-View-ViewModel) pattern with Prism framework and Unity dependency injection. ViewModels contain business logic and state management, binding to Views through XAML data binding. Common issues include property change notification problems, command binding failures, and event subscription memory leaks.

System Architecture

ViewModel Pattern:
┌─────────────┐     Data Binding     ┌─────────────┐
│    View     │◄────────────────────►│  ViewModel  │
│   (XAML)    │                      │   (.cs)     │
└─────────────┘                      └─────────────┘
     │                                     │
     │ Code-behind                         │
     ▼                                     ▼
┌─────────────┐     Services/Events     ┌─────────────┐
│   IView     │◄───────────────────────►│  Services   │
│  Interface  │                         │  (DI)       │
└─────────────┘                         └─────────────┘

Common ViewModel Issues and Solutions

Issue 1: Property Change Not Reflected in UI

Symptoms:

  • Property value changes but UI doesn't update
  • No visual feedback on data changes

Diagnosis:

// Problem: Missing PropertyChanged notification
public string Name 
{ 
    get => _name;
    set => _name = value;  // Missing notification!
}

Solution:

private string _name;
public string Name
{
    get => _name;
    set
    {
        if (_name != value)
        {
            _name = value;
            OnPropertyChanged(nameof(Name));
        }
    }
}

public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Issue 2: Command Not Executing

Symptoms:

  • Button click doesn't trigger action
  • Command's CanExecute always returns false

Diagnosis:

// Problem: Command not properly initialized or CanExecute not raising
public ICommand SaveCommand { get; set; }

// In constructor - missing initialization
SaveCommand = new DelegateCommand(OnSave);  // Missing CanExecute check

Solution:

// Using Prism's DelegateCommand
public ICommand SaveCommand { get; }

// In constructor
SaveCommand = new DelegateCommand(OnSave, CanSave);

private void OnSave()
{
    // Save logic
}

private bool CanSave()
{
    return !string.IsNullOrEmpty(Name) && HasChanges;
}

// Call when conditions change
private void RaiseCanExecuteChanged()
{
    (SaveCommand as DelegateCommand)?.RaiseCanExecuteChanged();
}

// Call after property changes that affect CanExecute
public string Name
{
    get => _name;
    set
    {
        if (_name != value)
        {
            _name = value;
            OnPropertyChanged(nameof(Name));
            RaiseCanExecuteChanged();  // Update command state
        }
    }
}

Issue 3: Event Subscription Memory Leak

Symptoms:

  • ViewModel not garbage collected
  • Multiple event handlers executing
  • Memory usage increasing

Diagnosis:

// Problem: Not unsubscribing from events
public MyViewModel(IEventAggregator eventAggregator)
{
    eventAggregator.GetEvent<DataChangedEvent>().Subscribe(OnDataChanged);
    // Missing: Keep subscriber reference for unsubscribe
}

Solution:

using Prism.Events;

private readonly IEventAggregator _eventAggregator;
private SubscriptionToken _dataChangedToken;

public MyViewModel(IEventAggregator eventAggregator)
{
    _eventAggregator = eventAggregator;
    
    // Keep subscription token
    _dataChangedToken = _eventAggregator.GetEvent<DataChangedEvent>()
        .Subscribe(OnDataChanged, ThreadOption.PublisherThread, false, 
                   data => data != null);
}

// Implement IDisposable
public void Dispose()
{
    if (_dataChangedToken != null)
    {
        _eventAggregator.GetEvent<DataChangedEvent>().Unsubscribe(_dataChangedToken);
    }
}

Issue 4: Thread Affinity Problems

Symptoms:

  • "The calling thread cannot access this object" exception
  • UI freezing during operations
  • Data updated on wrong thread

Diagnosis:

// Problem: Updating UI-bound property from background thread
Task.Run(() => {
    Status = "Processing...";  // Cross-thread violation
});

Solution:

using System.Windows.Threading;

// Option 1: Use Dispatcher
Task.Run(() => {
    Application.Current.Dispatcher.Invoke(() => {
        Status = "Processing...";
    });
});

// Option 2: Use ThreadOption in event subscription
_eventAggregator.GetEvent<DataChangedEvent>()
    .Subscribe(OnDataChanged, ThreadOption.UIThread);

// Option 3: Use async/await properly
public async Task LoadDataAsync()
{
    Status = "Loading...";  // On UI thread
    var data = await _service.GetDataAsync();  // Background
    Items = new ObservableCollection<DataItem>(data);  // Back on UI thread
    Status = "Complete";
}

Issue 5: Collection Changes Not Notifying

Symptoms:

  • Items added to collection don't appear in UI
  • ListView/DataGrid not updating

Diagnosis:

// Problem: Using List instead of ObservableCollection
public List<SensorItem> Sensors { get; set; }

Solution:

using System.Collections.ObjectModel;

// Use ObservableCollection for UI binding
public ObservableCollection<SensorItem> Sensors { get; }

public MyViewModel()
{
    Sensors = new ObservableCollection<SensorItem>();
}

// To add items from background thread:
Application.Current.Dispatcher.Invoke(() => {
    Sensors.Add(newItem);
});

// For bulk updates, clear and add range:
public void UpdateSensors(List<SensorItem> newItems)
{
    Sensors.Clear();
    foreach (var item in newItems)
    {
        Sensors.Add(item);
    }
}

Debugging Approach

Step 1: Check Property Implementation

// Add debug output to property setter
public string Name
{
    get => _name;
    set
    {
        System.Diagnostics.Debug.WriteLine($"Name: {_name} -> {value}");
        if (_name != value)
        {
            _name = value;
            OnPropertyChanged(nameof(Name));
            System.Diagnostics.Debug.WriteLine($"OnPropertyChanged raised for Name");
        }
    }
}

Step 2: Verify Binding in View

<!-- Add PresentationTraceSources for binding debug -->
<TextBox Text="{Binding Name, Mode=TwoWay, 
         diagnostics:PresentationTraceSources.TraceLevel=High}"
         xmlns:diagnostics="clr-namespace:System.Diagnostics;assembly=WindowsBase"/>

Step 3: Check DataContext

// In View code-behind, verify DataContext
public partial class MyView : UserControl
{
    public MyView()
    {
        InitializeComponent();
        this.DataContextChanged += (s, e) => 
        {
            System.Diagnostics.Debug.WriteLine($"DataContext: {e.NewValue?.GetType().Name}");
        };
    }
}

Step 4: Verify Event Subscriptions

// Add debug output to event handlers
private void OnDataChanged(DataChangedEventArgs args)
{
    System.Diagnostics.Debug.WriteLine($"OnDataChanged called: {args?.Data}");
    // ... handler logic
}

Step 5: Check Command Binding

// Add debug output to command methods
private void OnExecute()
{
    System.Diagnostics.Debug.WriteLine("OnExecute called");
}

private bool CanExecute()
{
    var result = !string.IsNullOrEmpty(Name);
    System.Diagnostics.Debug.WriteLine($"CanExecute: {result}");
    return result;
}

ViewModel Template Reference

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.Windows.Input;
using DTS.Common.Interface;
using Prism.Commands;
using Prism.Events;
using Prism.Regions;
using Unity;

namespace {NAMESPACE}
{
    [PartCreationPolicy(CreationPolicy.Shared)]
    public class {VIEWMODEL_NAME} : I{VIEWMODEL_NAME}, INotifyPropertyChanged, IDisposable
    {
        private readonly IEventAggregator _eventAggregator;
        private readonly IRegionManager _regionManager;
        private readonly IUnityContainer _unityContainer;
        
        public I{VIEW_NAME} View { get; set; }
        
        #region Properties with Change Notification
        
        private string _status;
        public string Status
        {
            get => _status;
            set
            {
                if (_status != value)
                {
                    _status = value;
                    OnPropertyChanged(nameof(Status));
                }
            }
        }
        
        #endregion
        
        #region Commands
        
        public ICommand SaveCommand { get; }
        public ICommand CancelCommand { get; }
        
        #endregion
        
        #region Collections
        
        public ObservableCollection<Item> Items { get; }
        
        #endregion
        
        public event PropertyChangedEventHandler PropertyChanged;
        
        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        
        public {VIEWMODEL_NAME}(
            I{VIEW_NAME} view,
            IRegionManager regionManager,
            IEventAggregator eventAggregator,
            IUnityContainer unityContainer)
        {
            View = view;
            View.DataContext = this;
            _regionManager = regionManager;
            _eventAggregator = eventAggregator;
            _unityContainer = unityContainer;
            
            // Initialize commands
            SaveCommand = new DelegateCommand(OnSave, CanSave);
            CancelCommand = new DelegateCommand(OnCancel);
            
            // Initialize collections
            Items = new ObservableCollection<Item>();
            
            // Subscribe to events
            SubscribeToEvents();
        }
        
        private void SubscribeToEvents()
        {
            // Event subscriptions
        }
        
        private void OnSave()
        {
            // Save implementation
        }
        
        private bool CanSave()
        {
            return true; // Add validation logic
        }
        
        private void OnCancel()
        {
            // Cancel implementation
        }
        
        public void Dispose()
        {
            // Unsubscribe from events
            // Dispose resources
        }
    }
}

Validation Checklist

After fixing a ViewModel bug:

  • PropertyChanged event raised for all bound properties
  • Commands use DelegateCommand with proper CanExecute
  • RaiseCanExecuteChanged called when command state changes
  • Event subscriptions use subscription tokens
  • Unsubscribe from events in Dispose
  • ObservableCollection used for collections
  • Dispatcher used for cross-thread updates
  • Async/await used for long-running operations
  • DataContext set correctly in constructor
  • View implements corresponding interface

Quick Reference: Common Patterns

Pattern Implementation
Property Notification OnPropertyChanged(nameof(PropertyName))
Command new DelegateCommand(Execute, CanExecute)
Event Subscription _eventAggregator.GetEvent<T>().Subscribe(Handler)
Thread-Safe Update Dispatcher.Invoke(() => Property = value)
Collection ObservableCollection<T>
Validation RaiseCanExecuteChanged() in property setters