Part 2: MVVM Flow Patterns in Detail
While MVC orchestrates sequential request-response cycles, MVVM creates a reactive mesh where changes propagate automatically through bindings. The IVVMM mnemonic hints at this bidirectional nature, but production MVVM applications weave together multiple simultaneous flows that would overwhelm a traditional controller-based architecture.
2.1 Direct View Flows
Visual State Management
MVVM allows views to manage their own visual states without involving the ViewModel:
<!-- XAML View with Visual State Manager -->
<UserControl x:Class="TradingApp.StockTickerView">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="PriceStates">
<VisualState x:Name="PriceUp">
<Storyboard>
<ColorAnimation Storyboard.TargetName="PricePanel"
Storyboard.TargetProperty="Background.Color"
To="LightGreen" Duration="0:0:0.3"/>
<DoubleAnimation Storyboard.TargetName="ArrowUp"
Storyboard.TargetProperty="Opacity"
To="1" Duration="0:0:0.3"/>
</Storyboard>
</VisualState>
<VisualState x:Name="PriceDown">
<Storyboard>
<ColorAnimation Storyboard.TargetName="PricePanel"
Storyboard.TargetProperty="Background.Color"
To="LightPink" Duration="0:0:0.3"/>
<DoubleAnimation Storyboard.TargetName="ArrowDown"
Storyboard.TargetProperty="Opacity"
To="1" Duration="0:0:0.3"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<!-- Triggers that create View → View flows -->
<i:Interaction.Triggers>
<ei:DataTrigger Binding="{Binding PriceDirection}" Value="Up">
<ei:GoToStateAction StateName="PriceUp"/>
</ei:DataTrigger>
<ei:DataTrigger Binding="{Binding PriceDirection}" Value="Down">
<ei:GoToStateAction StateName="PriceDown"/>
</ei:DataTrigger>
</i:Interaction.Triggers>
<Border x:Name="PricePanel">
<TextBlock Text="{Binding Price, StringFormat=C}"/>
</Border>
</Grid>
</UserControl>
This creates a flow where property changes trigger visual state transitions entirely within the view layer, keeping presentation logic out of the ViewModel.
Cascading UI Updates
Views can trigger cascading updates through pure declarative bindings:
<Window x:Class="DashboardApp.MainWindow">
<Grid>
<!-- Master selector that triggers cascading updates -->
<ComboBox x:Name="RegionSelector"
ItemsSource="{Binding Regions}"
SelectedItem="{Binding SelectedRegion}"/>
<!-- Multiple dependent views update automatically -->
<TabControl>
<TabItem Header="Sales"
Visibility="{Binding SelectedRegion.HasSalesData,
Converter={StaticResource BoolToVisibility}}">
<DataGrid ItemsSource="{Binding SelectedRegion.SalesData}"/>
</TabItem>
<TabItem Header="Inventory">
<!-- Nested cascading: Region → Warehouse → Products -->
<StackPanel>
<ComboBox ItemsSource="{Binding SelectedRegion.Warehouses}"
SelectedItem="{Binding SelectedWarehouse}"/>
<DataGrid ItemsSource="{Binding SelectedWarehouse.Products}">
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<!-- Even deeper nesting -->
<ItemsControl ItemsSource="{Binding StockMovements}"/>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>
</StackPanel>
</TabItem>
</TabControl>
<!-- Summary that aggregates from multiple paths -->
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="Region: {0}, Sales: {1:C}, Products: {2}">
<Binding Path="SelectedRegion.Name"/>
<Binding Path="SelectedRegion.TotalSales"/>
<Binding Path="SelectedWarehouse.Products.Count"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</Window>
Each selection change cascades through dependent bindings, creating complex View → View flows without any imperative code.
2.2 Command and Event Flows
Command Pattern Implementation
Commands in MVVM encapsulate actions with automatic UI state management:
public class OrderViewModel : ViewModelBase
{
private readonly IOrderService _orderService;
private bool _isProcessing;
public OrderViewModel(IOrderService orderService)
{
_orderService = orderService;
// Command with automatic CanExecute flow
SubmitOrderCommand = new RelayCommand(
execute: async () => await SubmitOrderAsync(),
canExecute: () => !_isProcessing && IsOrderValid()
);
// Property changes automatically re-evaluate CanExecute
PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(IsProcessing) ||
e.PropertyName == nameof(OrderItems))
{
SubmitOrderCommand.RaiseCanExecuteChanged();
}
};
}
public ICommand SubmitOrderCommand { get; }
private async Task SubmitOrderAsync()
{
// Flow: View → Command → ViewModel → Service → Model
IsProcessing = true;
try
{
var order = await _orderService.SubmitOrderAsync(OrderItems);
// Success triggers multiple flows
OrderConfirmation = order;
OrderItems.Clear();
await NotifySubscribersAsync(order);
}
catch (Exception ex)
{
// Error flow
ErrorMessage = ex.Message;
ErrorVisibility = Visibility.Visible;
}
finally
{
IsProcessing = false; // Re-enables command
}
}
public bool IsProcessing
{
get => _isProcessing;
set
{
if (SetProperty(ref _isProcessing, value))
{
// Triggers UI updates for loading indicators
OnPropertyChanged(nameof(LoadingVisibility));
OnPropertyChanged(nameof(InputEnabled));
}
}
}
}
// Async command with progress reporting
public class AsyncCommand<T> : ICommand
{
private readonly Func<T, IProgress<int>, Task> _execute;
private readonly Predicate<T> _canExecute;
private bool _isExecuting;
public event EventHandler CanExecuteChanged;
public event EventHandler<int> ProgressChanged;
public bool CanExecute(object parameter)
{
return !_isExecuting && (_canExecute?.Invoke((T)parameter) ?? true);
}
public async void Execute(object parameter)
{
_isExecuting = true;
RaiseCanExecuteChanged();
var progress = new Progress<int>(p =>
{
// Flow: Background Thread → Progress → UI Thread → View
ProgressChanged?.Invoke(this, p);
});
try
{
await _execute((T)parameter, progress);
}
finally
{
_isExecuting = false;
RaiseCanExecuteChanged();
}
}
}
External Service Integration
MVVM handles external service calls through commands with reactive updates:
public class WeatherViewModel : ViewModelBase
{
private readonly IWeatherService _weatherService;
private readonly ILocationService _locationService;
private CancellationTokenSource _refreshCts;
public WeatherViewModel()
{
RefreshCommand = new RelayCommand(async () => await RefreshWeatherAsync());
// Auto-refresh timer creates continuous flow
InitializeAutoRefresh();
}
private void InitializeAutoRefresh()
{
var timer = new DispatcherTimer { Interval = TimeSpan.FromMinutes(5) };
timer.Tick += async (s, e) =>
{
// Flow: Timer → ViewModel → Service → Model → View
if (AutoRefreshEnabled)
await RefreshWeatherAsync();
};
timer.Start();
}
private async Task RefreshWeatherAsync()
{
// Cancel previous refresh if still running
_refreshCts?.Cancel();
_refreshCts = new CancellationTokenSource();
try
{
IsRefreshing = true;
// Parallel service calls
var locationTask = _locationService.GetCurrentLocationAsync(_refreshCts.Token);
var alertsTask = _weatherService.GetAlertsAsync(_refreshCts.Token);
var location = await locationTask;
// Chain dependent service call
var weatherTask = _weatherService.GetWeatherAsync(location, _refreshCts.Token);
await Task.WhenAll(weatherTask, alertsTask);
// Update all properties atomically
await Application.Current.Dispatcher.InvokeAsync(() =>
{
CurrentWeather = weatherTask.Result;
WeatherAlerts = new ObservableCollection<Alert>(alertsTask.Result);
LastUpdated = DateTime.Now;
UpdateForecastChart();
});
}
catch (OperationCanceledException)
{
// Refresh was cancelled, no error to show
}
catch (Exception ex)
{
await ShowErrorAsync(ex);
}
finally
{
IsRefreshing = false;
}
}
}
Push Notifications
Model changes can push updates through ViewModels to Views:
public class StockPortfolioViewModel : ViewModelBase, IDisposable
{
private readonly IStockPriceService _priceService;
private readonly CompositeDisposable _subscriptions = new();
public ObservableCollection<StockViewModel> Stocks { get; }
public StockPortfolioViewModel(IStockPriceService priceService)
{
_priceService = priceService;
Stocks = new ObservableCollection<StockViewModel>();
// Subscribe to real-time price updates
var subscription = _priceService.PriceUpdates
.ObserveOnDispatcher()
.Subscribe(update =>
{
// Flow: External Event → Model → ViewModel → View
var stock = Stocks.FirstOrDefault(s => s.Symbol == update.Symbol);
if (stock != null)
{
var oldPrice = stock.Price;
stock.Price = update.Price;
stock.PriceDirection = update.Price > oldPrice ? "Up" : "Down";
// Trigger dependent calculations
RecalculatePortfolioValue();
UpdatePerformanceMetrics();
}
});
_subscriptions.Add(subscription);
}
private void RecalculatePortfolioValue()
{
// Aggregation triggers multiple property changes
var oldValue = TotalValue;
TotalValue = Stocks.Sum(s => s.Price * s.Quantity);
DayChange = TotalValue - oldValue;
DayChangePercent = (DayChange / oldValue) * 100;
// These property changes cascade to bound views
OnPropertyChanged(nameof(TotalValue));
OnPropertyChanged(nameof(DayChange));
OnPropertyChanged(nameof(DayChangePercent));
OnPropertyChanged(nameof(DayChangeColor));
}
}
2.3 Multi-ViewModel Communication
Messenger/Mediator Pattern
ViewModels communicate without direct references using messaging:
// Message types
public class OrderPlacedMessage
{
public Order Order { get; set; }
public DateTime Timestamp { get; set; }
}
public class InventoryUpdateMessage
{
public string ProductId { get; set; }
public int QuantityChange { get; set; }
}
// Publishing ViewModel
public class OrderViewModel : ViewModelBase
{
private readonly IMessenger _messenger;
private async Task PlaceOrderAsync()
{
var order = await _orderService.CreateOrderAsync(Items);
// Broadcast to all interested ViewModels
_messenger.Send(new OrderPlacedMessage
{
Order = order,
Timestamp = DateTime.Now
});
// Send targeted message to specific channel
foreach (var item in order.Items)
{
_messenger.Send(new InventoryUpdateMessage
{
ProductId = item.ProductId,
QuantityChange = -item.Quantity
}, "InventoryChannel");
}
}
}
// Subscribing ViewModels
public class DashboardViewModel : ViewModelBase
{
public DashboardViewModel(IMessenger messenger)
{
// Flow: ViewModel A → Messenger → ViewModel B → View
messenger.Register<OrderPlacedMessage>(this, async message =>
{
await Application.Current.Dispatcher.InvokeAsync(() =>
{
RecentOrders.Insert(0, new OrderSummary(message.Order));
TodaysSales += message.Order.Total;
UpdateChart();
});
});
}
}
public class InventoryViewModel : ViewModelBase
{
public InventoryViewModel(IMessenger messenger)
{
messenger.Register<InventoryUpdateMessage>(this, "InventoryChannel",
message =>
{
var product = Products.FirstOrDefault(p => p.Id == message.ProductId);
if (product != null)
{
product.StockLevel += message.QuantityChange;
if (product.StockLevel < product.ReorderPoint)
{
// Trigger another flow
messenger.Send(new LowStockAlert { Product = product });
}
}
});
}
}
Hierarchical ViewModels
Parent-child ViewModel relationships create structured flows:
public class MainViewModel : ViewModelBase
{
private ViewModelBase _currentPage;
public MainViewModel()
{
// Parent manages child lifecycle
NavigationCommand = new RelayCommand<string>(NavigateToPage);
// Initialize child ViewModels
Children = new Dictionary<string, Lazy<ViewModelBase>>
{
["Dashboard"] = new Lazy<ViewModelBase>(() => CreateDashboardViewModel()),
["Orders"] = new Lazy<ViewModelBase>(() => CreateOrdersViewModel()),
["Settings"] = new Lazy<ViewModelBase>(() => CreateSettingsViewModel())
};
}
private DashboardViewModel CreateDashboardViewModel()
{
var vm = new DashboardViewModel(_services);
// Child → Parent communication via events
vm.AlertRaised += (s, alert) =>
{
// Flow: Child ViewModel → Parent ViewModel → Parent View
ShowNotification(alert);
LogActivity($"Alert: {alert.Message}");
};
// Parent → Child communication via properties
vm.UserContext = CurrentUserContext;
return vm;
}
public ViewModelBase CurrentPage
{
get => _currentPage;
set
{
// Cleanup previous child
if (_currentPage is IDisposable disposable)
disposable.Dispose();
SetProperty(ref _currentPage, value);
// Initialize new child
if (value is IInitializable initializable)
initializable.InitializeAsync().FireAndForget();
}
}
}
// Child ViewModel with parent awareness
public class OrderDetailsViewModel : ViewModelBase
{
private readonly WeakReference<MainViewModel> _parentRef;
public OrderDetailsViewModel(MainViewModel parent)
{
// Weak reference prevents memory leaks
_parentRef = new WeakReference<MainViewModel>(parent);
SaveCommand = new RelayCommand(async () =>
{
await SaveOrderAsync();
// Navigate back through parent
if (_parentRef.TryGetTarget(out var parent))
{
parent.NavigateBack();
}
});
}
}
2.4 Service-Mediated Flows
Repository Pattern
ViewModels interact with data through repository abstraction:
public class CustomerManagementViewModel : ViewModelBase
{
private readonly ICustomerRepository _repository;
private readonly IUnitOfWork _unitOfWork;
public CustomerManagementViewModel(ICustomerRepository repository, IUnitOfWork unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
LoadCustomersCommand = new RelayCommand(async () => await LoadCustomersAsync());
SaveChangesCommand = new RelayCommand(
async () => await SaveChangesAsync(),
() => _unitOfWork.HasChanges
);
// Track changes for save button enable/disable
_unitOfWork.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(IUnitOfWork.HasChanges))
SaveChangesCommand.RaiseCanExecuteChanged();
};
}
private async Task LoadCustomersAsync()
{
// Flow: View → ViewModel → Repository → Cache/Database → Model
IsLoading = true;
try
{
// Repository handles caching transparently
var customers = await _repository.GetAllAsync(
include: c => c.Orders.ThenInclude(o => o.Items),
orderBy: q => q.OrderBy(c => c.Name),
useCache: true
);
// Wrap in ViewModels for UI binding
Customers = new ObservableCollection<CustomerViewModel>(
customers.Select(c => new CustomerViewModel(c, _unitOfWork))
);
// Set up collection change tracking
Customers.CollectionChanged += OnCustomersCollectionChanged;
}
finally
{
IsLoading = false;
}
}
private async Task SaveChangesAsync()
{
// Flow: ViewModel → UnitOfWork → Repository → Database → Cache Invalidation
try
{
var changes = _unitOfWork.GetChanges();
// Optimistic UI update
foreach (var change in changes)
{
if (change.State == EntityState.Deleted)
{
var vm = Customers.FirstOrDefault(c => c.Model.Id == change.Entity.Id);
if (vm != null) Customers.Remove(vm);
}
}
// Persist changes
await _unitOfWork.SaveChangesAsync();
// Refresh affected aggregates
await RefreshSummaryStatistics();
}
catch (DbUpdateConcurrencyException ex)
{
// Handle optimistic concurrency
await HandleConcurrencyConflict(ex);
}
}
}
Service Layer Round-Trip
Complex business operations require service orchestration:
public class CheckoutViewModel : ViewModelBase
{
private readonly IOrderService _orderService;
private readonly IPaymentService _paymentService;
private readonly IShippingService _shippingService;
public CheckoutViewModel()
{
ProcessCheckoutCommand = new RelayCommand(async () => await ProcessCheckoutAsync());
}
private async Task ProcessCheckoutAsync()
{
// Complex flow with multiple service interactions
var workflow = new CheckoutWorkflow();
try
{
// Step 1: Validate
CurrentStep = "Validating order...";
workflow.ValidationResult = await _orderService.ValidateOrderAsync(Cart);
if (!workflow.ValidationResult.IsValid)
{
ShowValidationErrors(workflow.ValidationResult.Errors);
return;
}
// Step 2: Calculate shipping
CurrentStep = "Calculating shipping...";
workflow.ShippingOptions = await _shippingService.CalculateShippingAsync(
Cart.Items,
ShippingAddress
);
// User selects shipping option (View interaction)
SelectedShipping = await ShowShippingSelectionDialog(workflow.ShippingOptions);
// Step 3: Process payment
CurrentStep = "Processing payment...";
workflow.PaymentResult = await _paymentService.ProcessPaymentAsync(
PaymentMethod,
Cart.Total + SelectedShipping.Cost
);
if (!workflow.PaymentResult.Success)
{
await RollbackShippingReservation(workflow);
ShowPaymentError(workflow.PaymentResult.Error);
return;
}
// Step 4: Create order
CurrentStep = "Creating order...";
workflow.Order = await _orderService.CreateOrderAsync(
Cart,
workflow.PaymentResult.TransactionId,
SelectedShipping
);
// Step 5: Success flow
await NavigateToConfirmation(workflow.Order);
}
catch (Exception ex)
{
await RollbackWorkflow(workflow);
ShowError(ex);
}
finally
{
CurrentStep = null;
}
}
}
2.5 Validation Flows
Inline Validation
Real-time validation with immediate feedback:
public class RegistrationViewModel : ValidatableViewModelBase
{
private string _email;
private string _password;
public RegistrationViewModel()
{
// Set up validation rules
AddValidationRule(() => Email,
email => IsValidEmail(email),
"Please enter a valid email address");
AddValidationRule(() => Password,
pwd => pwd?.Length >= 8,
"Password must be at least 8 characters");
AddValidationRule(() => PasswordConfirm,
confirm => confirm == Password,
"Passwords do not match");
// Async validation
AddAsyncValidationRule(() => Email,
async email => await IsEmailAvailableAsync(email),
"This email is already registered");
}
public string Email
{
get => _email;
set
{
if (SetProperty(ref _email, value))
{
// Flow: View → ViewModel → Validation Rules → ViewModel → View
ValidateProperty();
// Trigger dependent validations
if (HasValidationRule(() => EmailDomain))
ValidateProperty(nameof(EmailDomain));
}
}
}
private async Task<bool> IsEmailAvailableAsync(string email)
{
// Debounce API calls
await Task.Delay(300);
if (_validationCancellation.IsCancellationRequested)
return true;
return await _userService.CheckEmailAvailabilityAsync(email);
}
}
// Base class for validation
public abstract class ValidatableViewModelBase : ViewModelBase, INotifyDataErrorInfo
{
private readonly Dictionary<string, List<string>> _errors = new();
private readonly Dictionary<string, List<ValidationRule>> _validationRules = new();
public bool HasErrors => _errors.Any();
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public IEnumerable GetErrors(string propertyName)
{
return _errors.TryGetValue(propertyName, out var errors)
? errors
: Enumerable.Empty<string>();
}
protected void ValidateProperty([CallerMemberName] string propertyName = null)
{
// Clear existing errors
_errors.Remove(propertyName);
if (_validationRules.TryGetValue(propertyName, out var rules))
{
var errors = new List<string>();
foreach (var rule in rules)
{
if (!rule.Validate(GetPropertyValue(propertyName)))
errors.Add(rule.ErrorMessage);
}
if (errors.Any())
_errors[propertyName] = errors;
}
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
OnPropertyChanged(nameof(HasErrors));
}
}
Business Rule Validation
Domain-driven validation with complex rules:
public class LoanApplicationViewModel : ViewModelBase
{
private readonly ILoanValidationService _validationService;
public LoanApplicationViewModel(ILoanValidationService validationService)
{
_validationService = validationService;
// Property changes trigger validation cascade
PropertyChanged += async (s, e) =>
{
if (IsFinancialProperty(e.PropertyName))
{
await ValidateFinancialRulesAsync();
}
};
}
private async Task ValidateFinancialRulesAsync()
{
// Flow: ViewModel → Model → Business Rules → Exception → ViewModel → View
ValidationErrors.Clear();
try
{
var application = new LoanApplication
{
Income = AnnualIncome,
Expenses = MonthlyExpenses * 12,
LoanAmount = RequestedAmount,
CreditScore = CreditScore
};
// Domain model validates itself
application.Validate();
// External service validation
var serviceValidation = await _validationService.ValidateAsync(application);
if (!serviceValidation.IsEligible)
{
ValidationErrors.Add(new ValidationError
{
Property = "Eligibility",
Message = serviceValidation.Reason,
Severity = ValidationSeverity.Error
});
}
// Calculate derived values based on validation
if (serviceValidation.IsEligible)
{
MaxLoanAmount = serviceValidation.MaxAmount;
InterestRate = serviceValidation.Rate;
MonthlyPayment = CalculatePayment(RequestedAmount, InterestRate);
}
}
catch (DomainValidationException ex)
{
// Domain validation failed
foreach (var error in ex.Errors)
{
ValidationErrors.Add(new ValidationError
{
Property = error.Property,
Message = error.Message,
Severity = ValidationSeverity.Error
});
}
}
catch (BusinessRuleException ex)
{
// Business rule violation
ValidationErrors.Add(new ValidationError
{
Property = ex.Property,
Message = ex.Message,
Severity = ValidationSeverity.Warning
});
}
// Update UI state based on validation
IsValid = !ValidationErrors.Any(e => e.Severity == ValidationSeverity.Error);
CanSubmit = IsValid && IsComplete;
}
}
2.6 Asynchronous Flows
Background Operations
Managing long-running operations with progress reporting:
public class DataImportViewModel : ViewModelBase
{
private readonly BackgroundTaskManager _taskManager;
private CancellationTokenSource _importCts;
public DataImportViewModel()
{
ImportCommand = new RelayCommand(async () => await ImportDataAsync());
CancelCommand = new RelayCommand(() => _importCts?.Cancel(), () => IsImporting);
}
private async Task ImportDataAsync()
{
_importCts = new CancellationTokenSource();
IsImporting = true;
try
{
// Flow: View → ViewModel → Background Thread → Model → UI Thread → ViewModel → View
await Task.Run(async () =>
{
var files = await GetFilesAsync();
TotalItems = files.Sum(f => f.RecordCount);
foreach (var file in files)
{
CurrentFile = file.Name;
await ProcessFileAsync(file, progress =>
{
// Progress updates flow back to UI thread
Application.Current.Dispatcher.BeginInvoke(() =>
{
ProcessedItems += progress.ItemsProcessed;
CurrentProgress = (ProcessedItems / (double)TotalItems) * 100;
if (progress.HasError)
{
Errors.Add(progress.Error);
}
});
}, _importCts.Token);
}
}, _importCts.Token);
ImportResult = new ImportSummary
{
TotalProcessed = ProcessedItems,
Errors = Errors.ToList(),
Duration = DateTime.Now - _startTime
};
}
catch (OperationCanceledException)
{
StatusMessage = "Import cancelled by user";
}
catch (Exception ex)
{
StatusMessage = $"Import failed: {ex.Message}";
Logger.LogError(ex, "Data import failed");
}
finally
{
IsImporting = false;
_importCts?.Dispose();
}
}
}
// Background task coordination
public class BackgroundTaskManager
{
private readonly ConcurrentDictionary<Guid, TaskInfo> _runningTasks = new();
public async Task<T> RunAsync<T>(
Func<IProgress<TaskProgress>, CancellationToken, Task<T>> taskFunc,
Action<TaskProgress> progressCallback = null)
{
var taskId = Guid.NewGuid();
var cts = new CancellationTokenSource();
var progress = new Progress<TaskProgress>(p =>
{
// Ensure progress updates on UI thread
Application.Current.Dispatcher.BeginInvoke(() =>
{
progressCallback?.Invoke(p);
TaskProgressChanged?.Invoke(this, new TaskProgressEventArgs(taskId, p));
});
});
var taskInfo = new TaskInfo
{
Id = taskId,
StartTime = DateTime.Now,
CancellationTokenSource = cts
};
_runningTasks[taskId] = taskInfo;
try
{
return await taskFunc(progress, cts.Token);
}
finally
{
_runningTasks.TryRemove(taskId, out _);
cts.Dispose();
}
}
}
Promise/Task Patterns
Chaining asynchronous operations with reactive updates:
public class SearchViewModel : ViewModelBase
{
private readonly ISearchService _searchService;
private readonly TaskCompletionSource<SearchResults> _searchTcs;
private readonly Subject<string> _searchSubject;
public SearchViewModel(ISearchService searchService)
{
_searchService = searchService;
_searchSubject = new Subject<string>();
// Reactive search with debouncing
_searchSubject
.Throttle(TimeSpan.FromMilliseconds(300))
.DistinctUntilChanged()
.Select(query => Observable.FromAsync(async () => await SearchAsync(query)))
.Switch()
.ObserveOnDispatcher()
.Subscribe(
results => SearchResults = results,
error => HandleSearchError(error)
);
}
public string SearchQuery
{
get => _searchQuery;
set
{
if (SetProperty(ref _searchQuery, value))
{
_searchSubject.OnNext(value);
}
}
}
private async Task<SearchResults> SearchAsync(string query)
{
if (string.IsNullOrWhiteSpace(query))
return SearchResults.Empty;
IsSearching = true;
try
{
// Chain multiple async operations
var searchTask = _searchService.SearchAsync(query);
var suggestionsTask = _searchService.GetSuggestionsAsync(query);
var historyTask = _searchService.GetSearchHistoryAsync(query);
// Wait for primary result
var results = await searchTask;
// Continue with secondary operations
var secondaryTasks = Task.WhenAll(suggestionsTask, historyTask);
// Don't wait for secondary tasks to complete
_ = secondaryTasks.ContinueWith(t =>
{
if (t.Status == TaskStatus.RanToCompletion)
{
Application.Current.Dispatcher.BeginInvoke(() =>
{
Suggestions = suggestionsTask.Result;
RecentSearches = historyTask.Result;
});
}
});
return results;
}
finally
{
IsSearching = false;
}
}
}
2.7 External Event Flows
Real-Time Updates
Handling external events with reactive bindings:
public class LiveDashboardViewModel : ViewModelBase, IDisposable
{
private readonly IHubConnection _hubConnection;
private readonly CompositeDisposable _subscriptions = new();
public LiveDashboardViewModel()
{
InitializeRealTimeConnection();
}
private async void InitializeRealTimeConnection()
{
_hubConnection = new HubConnectionBuilder()
.WithUrl("https://api.example.com/live")
.WithAutomaticReconnect()
.Build();
// Flow: External Event → Model → ViewModel → View
_hubConnection.On<MetricUpdate>("MetricUpdated", update =>
{
Application.Current.Dispatcher.BeginInvoke(() =>
{
var metric = Metrics.FirstOrDefault(m => m.Id == update.MetricId);
if (metric != null)
{
// Animate value change
AnimateValue(metric, metric.Value, update.NewValue);
// Update trend
metric.Trend = CalculateTrend(metric.History);
// Trigger alerts if thresholds exceeded
CheckThresholds(metric, update.NewValue);
}
});
});
_hubConnection.Reconnecting += error =>
{
IsConnected = false;
ConnectionStatus = "Reconnecting...";
return Task.CompletedTask;
};
_hubConnection.Reconnected += connectionId =>
{
IsConnected = true;
ConnectionStatus = "Connected";
return RefreshDataAsync();
};
await _hubConnection.StartAsync();
}
private void AnimateValue(MetricViewModel metric, double oldValue, double newValue)
{
var animation = new DoubleAnimation
{
From = oldValue,
To = newValue,
Duration = TimeSpan.FromMilliseconds(500),
EasingFunction = new QuadraticEase()
};
animation.CurrentValueChanged += (s, e) =>
{
metric.DisplayValue = (double)animation.GetCurrentValue();
};
animation.Completed += (s, e) =>
{
metric.Value = newValue;
metric.LastUpdated = DateTime.Now;
};
animation.Begin();
}
}
Scheduled Updates
Periodic updates with automatic refresh:
public class MonitoringViewModel : ViewModelBase
{
private readonly Timer _refreshTimer;
private readonly Timer _cleanupTimer;
public MonitoringViewModel()
{
// Different timers for different update frequencies
_refreshTimer = new Timer(
callback: async _ => await RefreshMetricsAsync(),
state: null,
dueTime: TimeSpan.Zero,
period: TimeSpan.FromSeconds(30)
);
_cleanupTimer = new Timer(
callback: _ => CleanupOldData(),
state: null,
dueTime: TimeSpan.FromMinutes(5),
period: TimeSpan.FromMinutes(5)
);
// Adaptive refresh rate based on activity
PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(IsUserActive))
{
AdjustRefreshRate();
}
};
}
private void AdjustRefreshRate()
{
var period = IsUserActive
? TimeSpan.FromSeconds(10) // Faster when active
: TimeSpan.FromMinutes(1); // Slower when idle
_refreshTimer.Change(TimeSpan.Zero, period);
}
private async Task RefreshMetricsAsync()
{
// Skip if previous refresh still running
if (Interlocked.CompareExchange(ref _isRefreshing, 1, 0) == 1)
return;
try
{
var metrics = await _monitoringService.GetLatestMetricsAsync();
await Application.Current.Dispatcher.InvokeAsync(() =>
{
// Merge updates with existing data
foreach (var update in metrics)
{
var existing = Metrics.FirstOrDefault(m => m.Id == update.Id);
if (existing != null)
{
existing.Update(update);
}
else
{
Metrics.Add(new MetricViewModel(update));
}
}
LastRefreshed = DateTime.Now;
});
}
finally
{
Interlocked.Exchange(ref _isRefreshing, 0);
}
}
}
2.8 Converter/Transformer Flows
Value Conversion
Bidirectional value transformation between View and ViewModel:
public class CurrencyConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// Flow: ViewModel → Converter → View
if (value is decimal amount)
{
var currency = parameter as string ?? "USD";
return FormatCurrency(amount, currency);
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
// Flow: View → Converter → ViewModel
if (value is string text)
{
// Strip currency symbols and parse
var cleaned = Regex.Replace(text, @"[^\d.-]", "");
if (decimal.TryParse(cleaned, out var amount))
return amount;
}
return 0m;
}
}
// Multi-value converter for complex calculations
public class ProgressConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// Flow: Multiple ViewModels → Converter → View
if (values.Length >= 2 &&
values[0] is int completed &&
values[1] is int total &&
total > 0)
{
var percentage = (completed / (double)total) * 100;
return $"{percentage:F1}% ({completed}/{total})";
}
return "0%";
}
}
// Usage in XAML
/*
<ProgressBar>
<ProgressBar.Value>
<MultiBinding Converter="{StaticResource ProgressConverter}">
<Binding Path="CompletedTasks"/>
<Binding Path="TotalTasks"/>
</MultiBinding>
</ProgressBar.Value>
</ProgressBar>
*/
Multi-Binding
Complex property combinations through multi-binding:
public class ValidationStateConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// Combine multiple validation states
var hasErrors = values[0] as bool? ?? false;
var isValidating = values[1] as bool? ?? false;
var isDirty = values[2] as bool? ?? false;
if (isValidating)
return ValidationState.Validating;
if (hasErrors)
return ValidationState.Error;
if (isDirty)
return ValidationState.Modified;
return ValidationState.Valid;
}
}
// Advanced scenario: Computed properties with multiple sources
public class CompositeViewModel : ViewModelBase
{
// Properties from different sources
public ObservableCollection<OrderViewModel> Orders { get; }
public CustomerViewModel Customer { get; set; }
public DiscountSettings Discounts { get; set; }
// Computed property depends on multiple sources
public decimal TotalWithDiscount
{
get
{
var subtotal = Orders?.Sum(o => o.Total) ?? 0;
var customerDiscount = Customer?.LoyaltyDiscount ?? 0;
var seasonalDiscount = Discounts?.CurrentDiscount ?? 0;
var discount = Math.Max(customerDiscount, seasonalDiscount);
return subtotal * (1 - discount);
}
}
public CompositeViewModel()
{
// Set up property dependencies
Orders.CollectionChanged += (s, e) => OnPropertyChanged(nameof(TotalWithDiscount));
PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(Customer) || e.PropertyName == nameof(Discounts))
{
OnPropertyChanged(nameof(TotalWithDiscount));
// Subscribe to nested property changes
if (Customer != null)
{
Customer.PropertyChanged += (cs, ce) =>
{
if (ce.PropertyName == nameof(Customer.LoyaltyDiscount))
OnPropertyChanged(nameof(TotalWithDiscount));
};
}
}
};
}
}
Key Insights from MVVM Flow Patterns
After exploring these patterns, several key insights emerge:
Reactive Mesh vs Sequential Pipeline: Unlike MVC's sequential flows, MVVM creates a reactive mesh where changes can propagate in multiple directions simultaneously through bindings.
Declarative Complexity: Simple binding declarations can hide complex flow orchestration. A single property change might trigger cascading updates across multiple ViewModels and Views.
Asynchronous by Nature: MVVM naturally handles asynchronous operations through property notifications and commands, making it well-suited for responsive UIs with background operations.
Memory Management Challenges: The event-driven nature and strong references in bindings can create memory leaks if not carefully managed with weak references and proper disposal.
Testing Complexity: While ViewModels are testable in isolation, the full flow behavior including bindings, converters, and multi-ViewModel interactions requires sophisticated testing strategies.
Performance Implications: The automatic propagation of changes through bindings provides convenience but can trigger unnecessary updates if not carefully controlled with techniques like property change coalescing.
These patterns demonstrate why MVVM excels at building rich, interactive UIs where multiple views need to stay synchronized with complex state. The automatic change propagation through bindings eliminates much of the manual coordination code required in MVC, at the cost of increased complexity in understanding and debugging the complete flow of data through the application.
Conclusion: The Reactive Advantage
MVVM's flow patterns reveal a fundamentally different philosophy from the sequential, request-driven patterns we explored in MVC Flow Patterns in Detail. Where MVC provides explicit control and predictable execution order, MVVM offers automatic synchronization and reactive updates.
When MVVM Flows Excel
These patterns are particularly powerful for:
- Desktop applications with rich, stateful UIs (WPF, UWP, Avalonia)
- Mobile apps requiring offline capability and real-time updates (Xamarin, .NET MAUI)
- Complex dashboards with multiple interdependent views
- Data entry applications with extensive validation and calculated fields
- Real-time monitoring systems with live data feeds
The Cost of Reactivity
However, this power comes with trade-offs:
- Debugging complexity: Tracing data flow through bindings can be challenging
- Performance overhead: Excessive property notifications can cause UI stuttering
- Memory management: Event subscriptions require careful disposal
- Learning curve: Developers must think in terms of reactive streams rather than sequential operations
Looking Ahead
In our next article, we'll provide a detailed comparative analysis of MVC and MVVM flow patterns, examining performance implications, testing strategies, and decision frameworks for choosing between these architectural approaches.
For the foundational concepts behind these patterns, refer back to our MVC vs MVVM: what's the difference? (C# example) article, which introduces the ICMV and IVVMM mnemonics to help conceptualize these different flow philosophies.
Further Reading
- MVC vs MVVM: what's the difference? (C# example) - Foundational concepts and mnemonics
- MVC Flow Patterns in Detail - Sequential and request-driven patterns
- Comparative Analysis of MVC and MVVM Flows - Performance and decision frameworks (coming soon)
- Hybrid Patterns and Modern Frameworks - How contemporary frameworks blend both approaches (coming soon)
Top comments (0)