This commit is contained in:
2026-04-17 14:55:32 -04:00
commit bc3ac1d4c9
18017 changed files with 4371742 additions and 0 deletions

View File

@@ -0,0 +1,452 @@
# 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:**
```csharp
// Problem: Missing PropertyChanged notification
public string Name
{
get => _name;
set => _name = value; // Missing notification!
}
```
**Solution:**
```csharp
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:**
```csharp
// 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:**
```csharp
// 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:**
```csharp
// Problem: Not unsubscribing from events
public MyViewModel(IEventAggregator eventAggregator)
{
eventAggregator.GetEvent<DataChangedEvent>().Subscribe(OnDataChanged);
// Missing: Keep subscriber reference for unsubscribe
}
```
**Solution:**
```csharp
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:**
```csharp
// Problem: Updating UI-bound property from background thread
Task.Run(() => {
Status = "Processing..."; // Cross-thread violation
});
```
**Solution:**
```csharp
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:**
```csharp
// Problem: Using List instead of ObservableCollection
public List<SensorItem> Sensors { get; set; }
```
**Solution:**
```csharp
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
```csharp
// 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
```xml
<!-- 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
```csharp
// 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
```csharp
// 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
```csharp
// 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
```csharp
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 |