DEV Community

Dimension AI Technologies
Dimension AI Technologies

Posted on

MVC vs MVVM: Deep Dive into Real-World Flow Patterns - Part 2

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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 });
                }
            }
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
            }
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
*/
Enter fullscreen mode Exit fullscreen mode

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));
                    };
                }
            }
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Insights from MVVM Flow Patterns

After exploring these patterns, several key insights emerge:

  1. 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.

  2. Declarative Complexity: Simple binding declarations can hide complex flow orchestration. A single property change might trigger cascading updates across multiple ViewModels and Views.

  3. Asynchronous by Nature: MVVM naturally handles asynchronous operations through property notifications and commands, making it well-suited for responsive UIs with background operations.

  4. 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.

  5. Testing Complexity: While ViewModels are testable in isolation, the full flow behavior including bindings, converters, and multi-ViewModel interactions requires sophisticated testing strategies.

  6. 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

Top comments (0)