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)