CrackedRuby CrackedRuby

Model-View-ViewModel (MVVM)

Overview

Model-View-ViewModel (MVVM) is an architectural pattern that separates user interface code from business logic and data access code. The pattern emerged from Microsoft's development of Windows Presentation Foundation (WPF) and Silverlight, where data binding capabilities made the pattern practical. MVVM extends the Model-View-Controller pattern by introducing a ViewModel layer that acts as an intermediary between the View and Model.

The pattern divides application architecture into three components. The Model represents domain data and business logic. The View displays information to users and captures user input. The ViewModel exposes data from the Model in a format suitable for the View and handles presentation logic. Unlike traditional MVC where Controllers handle input, MVVM relies on data binding to synchronize the View and ViewModel automatically.

MVVM addresses the problem of tight coupling between user interface code and business logic. In applications without clear separation, UI code often contains business rules, making testing difficult and changes expensive. MVVM enforces separation by preventing direct references between Views and Models. The View knows only about the ViewModel, and the ViewModel knows only about the Model. This unidirectional dependency chain enables independent development and testing of each layer.

Data binding forms the core mechanism that makes MVVM practical. When a ViewModel property changes, the View updates automatically through the binding system. When a user interacts with the View, bound properties in the ViewModel update automatically. This bidirectional synchronization eliminates the manual glue code that couples Views to their data sources in other patterns.

# MVVM component interaction conceptual example
class UserViewModel
  attr_reader :user
  
  def initialize(user)
    @user = user
    @observers = []
  end
  
  def display_name
    "#{user.first_name} #{user.last_name}"
  end
  
  def update_email(new_email)
    user.email = new_email
    notify_observers(:email_changed)
  end
  
  private
  
  def notify_observers(event)
    @observers.each { |observer| observer.call(event) }
  end
end

MVVM particularly suits applications with complex user interfaces, frequent UI changes, or requirements for extensive automated testing. The pattern enables UI designers to work on Views while developers focus on ViewModels and Models. Teams can modify the View presentation without touching business logic, and vice versa.

Key Principles

Separation of Concerns divides the application into distinct layers with specific responsibilities. The Model layer handles data persistence, validation, and business rules. The View layer renders the user interface and captures user input. The ViewModel layer transforms Model data into formats suitable for display and translates user actions into Model operations. Each layer can change independently without affecting others when the contracts between layers remain stable.

Data Binding synchronizes data between Views and ViewModels without explicit code. Property binding connects UI elements to ViewModel properties. When the ViewModel property changes, the UI updates automatically. When the user modifies a bound UI element, the ViewModel property updates automatically. This eliminates the manual update code that clutters traditional UI implementations. Data binding requires an observable property mechanism where properties notify observers when values change.

Commands encapsulate user actions as objects rather than event handlers. Instead of Views calling ViewModel methods directly, Views execute commands exposed by ViewModels. Commands contain the action logic and determine when they can execute. A SaveCommand might disable itself when data is invalid, causing the bound UI button to disable automatically. This separates the decision logic from the UI event handling.

# Command pattern in MVVM
class Command
  def initialize(&action)
    @action = action
    @can_execute = true
  end
  
  def execute
    @action.call if can_execute?
  end
  
  def can_execute?
    @can_execute
  end
  
  def set_can_execute(value)
    @can_execute = value
  end
end

class ArticleViewModel
  attr_reader :save_command
  
  def initialize(article)
    @article = article
    @save_command = Command.new { save_article }
    update_command_state
  end
  
  def title=(value)
    @article.title = value
    update_command_state
  end
  
  private
  
  def save_article
    @article.save
  end
  
  def update_command_state
    @save_command.set_can_execute(@article.valid?)
  end
end

Testability improves because ViewModels contain no references to UI frameworks. Tests can instantiate ViewModels, set properties, execute commands, and verify results without rendering any UI components. This enables fast, reliable unit tests for presentation logic. The Model layer remains testable through traditional unit tests. Only the View requires UI testing, which minimizes the need for slow, brittle UI automation tests.

Presentation Logic resides in ViewModels rather than Views or Models. Logic for formatting data for display, aggregating data from multiple Models, handling user input validation, and managing UI state belongs in the ViewModel. Views contain only the minimal code required to render bound data and route user interactions to commands. Models contain only domain logic and data persistence code.

View Ignorance means ViewModels contain no references to Views or UI frameworks. ViewModels expose properties and commands but never call View methods or import View classes. This enables ViewModels to work with any View implementation, making it possible to swap UI technologies without changing ViewModels. The pattern maintains this separation through observer patterns and data binding rather than direct method calls.

# ViewModel with no View dependencies
class ProductListViewModel
  attr_reader :products, :selected_product
  
  def initialize(product_repository)
    @product_repository = product_repository
    @products = []
    @selected_product = nil
    @observers = {}
    load_products
  end
  
  def select_product(product)
    @selected_product = product
    notify(:product_selected, product)
  end
  
  def filter_by_category(category)
    @products = @product_repository.find_by_category(category)
    notify(:products_changed, @products)
  end
  
  def add_observer(event, &block)
    @observers[event] ||= []
    @observers[event] << block
  end
  
  private
  
  def load_products
    @products = @product_repository.all
    notify(:products_changed, @products)
  end
  
  def notify(event, data)
    return unless @observers[event]
    @observers[event].each { |observer| observer.call(data) }
  end
end

Model Independence ensures ViewModels aggregate and transform Model data but never contain business rules. When a business rule changes, only the Model changes. ViewModels adapt to Model changes but don't duplicate domain logic. This prevents the logic duplication that makes applications fragile and difficult to maintain.

Ruby Implementation

Ruby lacks native data binding frameworks found in WPF or Silverlight, but Ruby's metaprogramming capabilities enable MVVM implementations. The Observer pattern combined with method_missing or define_method creates observable properties. Ruby's blocks and procs implement command objects cleanly. Libraries like Glimmer provide data binding for desktop Ruby applications.

Observable Properties notify observers when values change. Ruby's attr_accessor creates simple getters and setters, but observable properties require notification logic. A custom implementation overrides the setter to trigger notifications.

module Observable
  def self.included(base)
    base.extend(ClassMethods)
  end
  
  module ClassMethods
    def observable_attr(*attributes)
      attributes.each do |attr|
        define_method(attr) do
          instance_variable_get("@#{attr}")
        end
        
        define_method("#{attr}=") do |value|
          old_value = instance_variable_get("@#{attr}")
          return if old_value == value
          
          instance_variable_set("@#{attr}", value)
          notify_observers(attr, old_value, value)
        end
      end
    end
  end
  
  def add_observer(&block)
    @observers ||= []
    @observers << block
  end
  
  def notify_observers(property, old_value, new_value)
    return unless @observers
    @observers.each do |observer|
      observer.call(property, old_value, new_value)
    end
  end
end

class UserViewModel
  include Observable
  
  observable_attr :first_name, :last_name, :email
  
  def initialize(user)
    @user = user
    @first_name = user.first_name
    @last_name = user.last_name
    @email = user.email
  end
  
  def full_name
    "#{first_name} #{last_name}"
  end
  
  def save
    @user.first_name = first_name
    @user.last_name = last_name
    @user.email = email
    @user.save
  end
end

# Usage with observer
view_model = UserViewModel.new(user)
view_model.add_observer do |property, old_val, new_val|
  puts "#{property} changed from #{old_val} to #{new_val}"
end

view_model.first_name = "Jane"
# Output: first_name changed from John to Jane

Rails and MVVM differs from traditional MVVM because Rails views use server-side rendering rather than client-side data binding. Rails controllers act partially as ViewModels by preparing data for views, but they also handle routing and HTTP concerns. A pure MVVM approach in Rails extracts presentation logic into explicit ViewModel objects.

# Rails ViewModel (Presenter pattern)
class ArticleViewModel
  delegate :title, :author, :created_at, to: :@article
  
  def initialize(article, current_user)
    @article = article
    @current_user = current_user
  end
  
  def formatted_date
    created_at.strftime("%B %d, %Y")
  end
  
  def can_edit?
    @current_user.admin? || @article.author == @current_user
  end
  
  def summary
    return @article.body if @article.body.length <= 100
    "#{@article.body[0..97]}..."
  end
  
  def tag_list
    @article.tags.map(&:name).join(", ")
  end
end

# In controller
class ArticlesController < ApplicationController
  def show
    article = Article.find(params[:id])
    @view_model = ArticleViewModel.new(article, current_user)
  end
end

# In view
<h1><%= @view_model.title %></h1>
<p>By <%= @view_model.author.name %> on <%= @view_model.formatted_date %></p>
<% if @view_model.can_edit? %>
  <%= link_to "Edit", edit_article_path(@view_model) %>
<% end %>
<p><%= @view_model.summary %></p>
<p>Tags: <%= @view_model.tag_list %></p>

Desktop Ruby Applications benefit more directly from MVVM. Glimmer provides data binding for SWT, GTK, Tk, and other GUI toolkits. The framework handles property synchronization and command binding automatically.

# Glimmer DSL for SWT example
require 'glimmer-dsl-swt'

class ContactViewModel
  attr_accessor :first_name, :last_name, :email
  
  def initialize
    @first_name = ""
    @last_name = ""
    @email = ""
  end
  
  def full_name
    "#{first_name} #{last_name}".strip
  end
  
  def valid?
    !first_name.empty? && !last_name.empty? && email.include?("@")
  end
end

include Glimmer

view_model = ContactViewModel.new

shell {
  text "Contact Form"
  
  composite {
    grid_layout 2, false
    
    label { text "First Name:" }
    text {
      layout_data :fill, :center, true, false
      text <=> [view_model, :first_name]
    }
    
    label { text "Last Name:" }
    text {
      layout_data :fill, :center, true, false
      text <=> [view_model, :last_name]
    }
    
    label { text "Email:" }
    text {
      layout_data :fill, :center, true, false
      text <=> [view_model, :email]
    }
    
    button {
      text "Save"
      enabled <= [view_model, :valid?]
      on_widget_selected { puts "Saving: #{view_model.full_name}" }
    }
  }
}.open

Sinatra with Client-Side MVVM separates concerns by using Sinatra for API endpoints and a JavaScript MVVM framework for the UI. Ruby ViewModels serialize Model data to JSON for API responses. This approach works well for single-page applications.

require 'sinatra'
require 'json'

class TaskViewModel
  def initialize(task)
    @task = task
  end
  
  def to_json(*args)
    {
      id: @task.id,
      title: @task.title,
      completed: @task.completed?,
      priority: priority_label,
      due_date: formatted_due_date,
      can_complete: can_complete?
    }.to_json(*args)
  end
  
  private
  
  def priority_label
    case @task.priority
    when 1 then "High"
    when 2 then "Medium"
    when 3 then "Low"
    else "None"
    end
  end
  
  def formatted_due_date
    return nil unless @task.due_date
    @task.due_date.strftime("%Y-%m-%d")
  end
  
  def can_complete?
    !@task.completed? && @task.assigned_to.present?
  end
end

get '/api/tasks' do
  content_type :json
  tasks = Task.all
  tasks.map { |task| TaskViewModel.new(task) }.to_json
end

get '/api/tasks/:id' do
  content_type :json
  task = Task.find(params[:id])
  TaskViewModel.new(task).to_json
end

Testing Ruby ViewModels requires no special frameworks since ViewModels contain no UI dependencies. Standard testing tools like RSpec or Minitest work directly with ViewModels.

require 'rspec'

RSpec.describe ArticleViewModel do
  let(:user) { double('User', admin?: false) }
  let(:article) do
    double('Article',
      title: "Test Article",
      body: "This is a test article body that is quite long.",
      created_at: Time.new(2025, 1, 15),
      author: user,
      tags: [double(name: "Ruby"), double(name: "Testing")]
    )
  end
  
  subject { ArticleViewModel.new(article, user) }
  
  describe '#formatted_date' do
    it 'formats date correctly' do
      expect(subject.formatted_date).to eq("January 15, 2025")
    end
  end
  
  describe '#can_edit?' do
    context 'when user is author' do
      let(:article) { double('Article', author: user) }
      
      it 'returns true' do
        expect(subject.can_edit?).to be true
      end
    end
    
    context 'when user is not author' do
      let(:different_user) { double('User', admin?: false) }
      let(:article) { double('Article', author: different_user) }
      
      it 'returns false' do
        expect(subject.can_edit?).to be false
      end
    end
  end
  
  describe '#tag_list' do
    it 'joins tag names with commas' do
      expect(subject.tag_list).to eq("Ruby, Testing")
    end
  end
end

Design Considerations

Pattern Selection depends on application requirements, team expertise, and platform capabilities. MVVM suits applications with complex UIs, frequent UI changes, or strong testing requirements. The pattern adds architectural overhead that small applications may not need. MVC often suffices for server-rendered web applications where data binding provides limited value. MVP works better than MVVM when the platform lacks data binding support and implementing binding manually would add unnecessary complexity.

Data Binding Complexity increases with MVVM adoption. Platforms without native binding require custom implementations that add code and potential bugs. Ruby's lack of built-in binding means teams must build or adopt binding frameworks. This infrastructure cost makes sense for large applications but burdens small projects. Teams must maintain binding implementations and debug binding-related issues alongside business logic.

Learning Curve affects team productivity during MVVM adoption. Developers familiar with MVC must learn ViewModel concepts, binding syntax, and command patterns. The mental shift from imperative UI updates to declarative binding takes time. Teams transitioning to MVVM should expect reduced velocity initially while members gain proficiency. Organizations should consider whether the long-term benefits justify the transition cost.

State Management becomes more explicit in MVVM. ViewModels centralize presentation state that might scatter across controllers and views in MVC. This centralization simplifies debugging and testing but requires discipline. Developers must resist placing business logic in ViewModels or UI logic in Models. Clear boundaries between layers need documentation and code review enforcement.

# State management in ViewModel
class OrderViewModel
  attr_reader :order, :editing, :saving
  
  def initialize(order)
    @order = order
    @editing = false
    @saving = false
    @observers = {}
  end
  
  def start_editing
    return if editing
    @editing = true
    @original_values = {
      quantity: order.quantity,
      shipping_address: order.shipping_address
    }
    notify(:state_changed)
  end
  
  def cancel_editing
    return unless editing
    @editing = false
    order.quantity = @original_values[:quantity]
    order.shipping_address = @original_values[:shipping_address]
    notify(:state_changed)
  end
  
  def save
    return unless editing && order.valid?
    @saving = true
    notify(:state_changed)
    
    order.save
    @editing = false
    @saving = false
    notify(:state_changed)
  end
  
  def can_save?
    editing && !saving && order.valid?
  end
end

Testing Strategy changes with MVVM adoption. The pattern enables more unit tests and fewer integration tests. ViewModels test independently from Views and Models through mocking. This fast feedback cycle improves development speed. However, teams still need integration tests to verify View-ViewModel binding works correctly. The testing pyramid shifts toward more unit tests, but integration testing remains necessary.

Platform Constraints determine MVVM viability. Platforms with mature data binding (WPF, Xamarin, SwiftUI) make MVVM natural. Web applications using frameworks like Vue, React, or Angular implement MVVM-like patterns through their binding systems. Server-rendered web frameworks like Rails gain less from MVVM because page refreshes break binding continuity. Desktop Ruby applications need explicit binding libraries. Mobile Ruby applications using RubyMotion can implement MVVM but lack the ecosystem support found in Swift or Kotlin.

Team Structure influences pattern choice. Organizations with separate UI designers and developers benefit from MVVM's separation. Designers modify Views while developers change ViewModels and Models independently. Small teams where developers handle both UI and logic may find MVC simpler. The coordination overhead of maintaining three layers instead of two only pays off when different people work on different layers.

Performance Trade-offs exist in binding-heavy applications. Each property change triggers notification propagation and UI updates. Complex views with hundreds of bindings can cause performance issues. Developers must batch updates, throttle notifications, or use virtual scrolling for large data sets. MVVM without careful performance consideration leads to sluggish UIs. The pattern doesn't inherently cause performance problems, but binding convenience can hide inefficiencies.

# Batching updates in ViewModel
class ProductGridViewModel
  def initialize(products)
    @products = products
    @observers = []
    @update_queue = []
    @batch_mode = false
  end
  
  def batch_update
    @batch_mode = true
    yield
    @batch_mode = false
    flush_updates
  end
  
  def update_product_price(product_id, new_price)
    product = @products.find { |p| p.id == product_id }
    product.price = new_price
    queue_update(:product_changed, product)
  end
  
  def update_product_stock(product_id, new_stock)
    product = @products.find { |p| p.id == product_id }
    product.stock = new_stock
    queue_update(:product_changed, product)
  end
  
  private
  
  def queue_update(event, data)
    if @batch_mode
      @update_queue << [event, data]
    else
      notify_observers(event, data)
    end
  end
  
  def flush_updates
    @update_queue.each do |event, data|
      notify_observers(event, data)
    end
    @update_queue.clear
  end
end

# Usage
view_model.batch_update do
  view_model.update_product_price(1, 29.99)
  view_model.update_product_stock(1, 50)
  view_model.update_product_price(2, 39.99)
end
# UI updates once instead of three times

Code Organization requires more files and classes in MVVM. Each Model may have corresponding ViewModels for different views. A User model might need UserListItemViewModel, UserDetailViewModel, and UserEditViewModel. This granularity improves testability but increases the codebase size. Directory structure should clearly separate the three layers to prevent accidental cross-layer dependencies.

Common Patterns

Property Notification implements the Observer pattern at the property level. ViewModels inherit from an observable base class or include an observable module. When properties change, the ViewModel notifies all registered observers. Views register as observers during initialization and update themselves when notifications arrive.

class ObservableViewModel
  def initialize
    @property_observers = Hash.new { |h, k| h[k] = [] }
  end
  
  def observe_property(property, &block)
    @property_observers[property] << block
  end
  
  def notify_property_changed(property)
    @property_observers[property].each(&:call)
  end
  
  protected
  
  def set_property(name, value)
    ivar = "@#{name}"
    old_value = instance_variable_get(ivar)
    return if old_value == value
    
    instance_variable_set(ivar, value)
    notify_property_changed(name)
  end
end

class TaskViewModel < ObservableViewModel
  def initialize(task)
    super()
    @task = task
    @title = task.title
    @completed = task.completed
  end
  
  def title
    @title
  end
  
  def title=(value)
    set_property(:title, value)
  end
  
  def completed
    @completed
  end
  
  def toggle_completed
    set_property(:completed, !@completed)
  end
end

Computed Properties derive values from other properties and update automatically when dependencies change. A full_name property that combines first_name and last_name must update when either name changes. ViewModels track property dependencies and trigger computed property notifications when dependencies change.

class ContactViewModel < ObservableViewModel
  attr_reader :first_name, :last_name
  
  def initialize(contact)
    super()
    @contact = contact
    @first_name = contact.first_name
    @last_name = contact.last_name
  end
  
  def first_name=(value)
    set_property(:first_name, value)
    notify_property_changed(:full_name)
    notify_property_changed(:initials)
  end
  
  def last_name=(value)
    set_property(:last_name, value)
    notify_property_changed(:full_name)
    notify_property_changed(:initials)
  end
  
  def full_name
    "#{first_name} #{last_name}".strip
  end
  
  def initials
    "#{first_name&.first}#{last_name&.first}".upcase
  end
end

Command Pattern encapsulates user actions with execution logic and availability logic. Commands expose a can_execute method that determines whether the command is currently valid. UI elements bind their enabled state to can_execute, causing buttons to disable when commands cannot execute.

class RelayCommand
  def initialize(execute_proc, can_execute_proc = nil)
    @execute_proc = execute_proc
    @can_execute_proc = can_execute_proc
    @can_execute_changed_observers = []
  end
  
  def execute
    @execute_proc.call if can_execute?
  end
  
  def can_execute?
    return true unless @can_execute_proc
    @can_execute_proc.call
  end
  
  def notify_can_execute_changed
    @can_execute_changed_observers.each(&:call)
  end
  
  def observe_can_execute_changed(&block)
    @can_execute_changed_observers << block
  end
end

class InvoiceViewModel
  attr_reader :approve_command, :reject_command
  
  def initialize(invoice)
    @invoice = invoice
    
    @approve_command = RelayCommand.new(
      -> { approve_invoice },
      -> { can_approve? }
    )
    
    @reject_command = RelayCommand.new(
      -> { reject_invoice },
      -> { can_reject? }
    )
  end
  
  def status=(value)
    @invoice.status = value
    @approve_command.notify_can_execute_changed
    @reject_command.notify_can_execute_changed
  end
  
  private
  
  def approve_invoice
    @invoice.approve!
    status = @invoice.status
  end
  
  def reject_invoice
    @invoice.reject!
    status = @invoice.status
  end
  
  def can_approve?
    @invoice.status == 'pending'
  end
  
  def can_reject?
    @invoice.status == 'pending'
  end
end

Collection ViewModels manage lists of items where each item has its own ViewModel. A list of products displays using ProductItemViewModel instances managed by a ProductListViewModel. The parent ViewModel handles collection operations like filtering, sorting, and selection while child ViewModels handle individual item presentation.

class ProductListViewModel
  attr_reader :products, :selected_product
  
  def initialize(product_repository)
    @product_repository = product_repository
    @products = []
    @selected_product = nil
    @filter = nil
    @observers = {}
    reload_products
  end
  
  def select_product(product_view_model)
    @selected_product = product_view_model
    notify(:selection_changed)
  end
  
  def filter_by_category(category)
    @filter = category
    reload_products
  end
  
  def clear_filter
    @filter = nil
    reload_products
  end
  
  def add_observer(event, &block)
    @observers[event] ||= []
    @observers[event] << block
  end
  
  private
  
  def reload_products
    models = @filter ? 
      @product_repository.find_by_category(@filter) :
      @product_repository.all
    
    @products = models.map { |model| ProductItemViewModel.new(model) }
    notify(:products_changed)
  end
  
  def notify(event)
    @observers[event]&.each(&:call)
  end
end

class ProductItemViewModel
  attr_reader :id, :name, :price, :in_stock
  
  def initialize(product)
    @product = product
    @id = product.id
    @name = product.name
    @price = product.price
    @in_stock = product.stock > 0
  end
  
  def display_price
    "$#{'%.2f' % price}"
  end
  
  def stock_status
    in_stock ? "Available" : "Out of Stock"
  end
end

Dependency Injection supplies dependencies to ViewModels through constructor parameters rather than global state or singletons. This pattern enables testing with mock repositories and makes dependencies explicit. A UserViewModel receives a UserRepository rather than calling User.find directly.

class UserRepository
  def find(id)
    User.find(id)
  end
  
  def save(user)
    user.save
  end
end

class UserEditViewModel
  def initialize(user_id, user_repository = UserRepository.new)
    @user_repository = user_repository
    @user = @user_repository.find(user_id)
    @first_name = @user.first_name
    @last_name = @user.last_name
    @email = @user.email
  end
  
  def save
    @user.first_name = @first_name
    @user.last_name = @last_name
    @user.email = @email
    @user_repository.save(@user)
  end
end

# Testing with mock repository
class MockUserRepository
  def find(id)
    OpenStruct.new(id: id, first_name: "Test", last_name: "User", email: "test@example.com")
  end
  
  def save(user)
    true
  end
end

# Test
repo = MockUserRepository.new
view_model = UserEditViewModel.new(1, repo)
view_model.save

Value Converters transform data between Model and View formats. A date stored as a DateTime object needs conversion to a formatted string for display. Converters handle this transformation bidirectionally, converting to display format when loading and parsing back to storage format when saving.

class Converter
  def convert(value)
    value
  end
  
  def convert_back(value)
    value
  end
end

class DateConverter < Converter
  def convert(date)
    return "" unless date
    date.strftime("%m/%d/%Y")
  end
  
  def convert_back(string)
    return nil if string.empty?
    Date.strptime(string, "%m/%d/%Y")
  rescue ArgumentError
    nil
  end
end

class CurrencyConverter < Converter
  def convert(amount)
    return "$0.00" unless amount
    "$#{'%.2f' % amount}"
  end
  
  def convert_back(string)
    string.gsub(/[$,]/, "").to_f
  end
end

class EventViewModel
  def initialize(event, date_converter = DateConverter.new)
    @event = event
    @date_converter = date_converter
  end
  
  def event_date
    @date_converter.convert(@event.date)
  end
  
  def event_date=(value)
    @event.date = @date_converter.convert_back(value)
  end
end

Navigation Services decouple ViewModels from navigation logic. Instead of ViewModels creating or showing views directly, they call navigation services that handle view instantiation and display. This maintains View ignorance in ViewModels while still enabling navigation.

class NavigationService
  def initialize
    @navigation_stack = []
  end
  
  def navigate_to(view_model)
    @navigation_stack.push(view_model)
    show_view_for(view_model)
  end
  
  def navigate_back
    return if @navigation_stack.size <= 1
    @navigation_stack.pop
    show_view_for(@navigation_stack.last)
  end
  
  def can_navigate_back?
    @navigation_stack.size > 1
  end
  
  private
  
  def show_view_for(view_model)
    # Framework-specific view instantiation and display
  end
end

class MainViewModel
  def initialize(navigation_service)
    @navigation_service = navigation_service
    
    @open_settings_command = RelayCommand.new(
      -> { open_settings }
    )
  end
  
  private
  
  def open_settings
    @navigation_service.navigate_to(SettingsViewModel.new(@navigation_service))
  end
end

Practical Examples

User Profile Management demonstrates a complete MVVM implementation for editing user profile data with validation and state management.

class User
  attr_accessor :id, :username, :email, :bio, :avatar_url
  
  def initialize(attributes = {})
    @id = attributes[:id]
    @username = attributes[:username]
    @email = attributes[:email]
    @bio = attributes[:bio]
    @avatar_url = attributes[:avatar_url]
  end
  
  def save
    # Database persistence logic
    true
  end
end

class UserRepository
  def find(id)
    # Simulated database fetch
    User.new(
      id: id,
      username: "johndoe",
      email: "john@example.com",
      bio: "Software developer",
      avatar_url: "https://example.com/avatar.jpg"
    )
  end
  
  def save(user)
    user.save
  end
  
  def username_exists?(username, exclude_id = nil)
    # Simulated uniqueness check
    username == "taken" && exclude_id != 1
  end
end

module Observable
  def add_observer(&block)
    @observers ||= []
    @observers << block
  end
  
  def notify_observers(property)
    return unless @observers
    @observers.each { |observer| observer.call(property) }
  end
end

class UserProfileViewModel
  include Observable
  
  attr_reader :user, :save_command, :cancel_command
  attr_accessor :username, :email, :bio
  
  def initialize(user_id, user_repository = UserRepository.new)
    @user_repository = user_repository
    @user = @user_repository.find(user_id)
    
    @original_state = {
      username: @user.username,
      email: @user.email,
      bio: @user.bio
    }
    
    @username = @user.username
    @email = @user.email
    @bio = @user.bio
    
    @validation_errors = {}
    
    @save_command = RelayCommand.new(
      -> { save },
      -> { can_save? }
    )
    
    @cancel_command = RelayCommand.new(
      -> { cancel }
    )
  end
  
  def username=(value)
    @username = value
    validate_username
    @save_command.notify_can_execute_changed
    notify_observers(:username)
  end
  
  def email=(value)
    @email = value
    validate_email
    @save_command.notify_can_execute_changed
    notify_observers(:email)
  end
  
  def bio=(value)
    @bio = value
    notify_observers(:bio)
  end
  
  def has_changes?
    username != @original_state[:username] ||
      email != @original_state[:email] ||
      bio != @original_state[:bio]
  end
  
  def username_error
    @validation_errors[:username]
  end
  
  def email_error
    @validation_errors[:email]
  end
  
  def avatar_url
    @user.avatar_url
  end
  
  private
  
  def can_save?
    has_changes? && @validation_errors.empty?
  end
  
  def save
    @user.username = username
    @user.email = email
    @user.bio = bio
    
    if @user_repository.save(@user)
      @original_state = {
        username: username,
        email: email,
        bio: bio
      }
      @save_command.notify_can_execute_changed
      notify_observers(:saved)
    end
  end
  
  def cancel
    self.username = @original_state[:username]
    self.email = @original_state[:email]
    self.bio = @original_state[:bio]
    @validation_errors.clear
    @save_command.notify_can_execute_changed
  end
  
  def validate_username
    @validation_errors.delete(:username)
    
    if username.nil? || username.strip.empty?
      @validation_errors[:username] = "Username cannot be blank"
    elsif username.length < 3
      @validation_errors[:username] = "Username must be at least 3 characters"
    elsif @user_repository.username_exists?(username, @user.id)
      @validation_errors[:username] = "Username is already taken"
    end
    
    notify_observers(:username_error)
  end
  
  def validate_email
    @validation_errors.delete(:email)
    
    if email.nil? || email.strip.empty?
      @validation_errors[:email] = "Email cannot be blank"
    elsif !email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
      @validation_errors[:email] = "Email format is invalid"
    end
    
    notify_observers(:email_error)
  end
end

# Console-based view simulation
view_model = UserProfileViewModel.new(1)

view_model.add_observer do |property|
  case property
  when :username
    puts "Username updated: #{view_model.username}"
    puts "Error: #{view_model.username_error}" if view_model.username_error
  when :email
    puts "Email updated: #{view_model.email}"
    puts "Error: #{view_model.email_error}" if view_model.email_error
  when :saved
    puts "Profile saved successfully!"
  end
end

# User interaction simulation
view_model.username = "jd"  # Triggers validation error
view_model.username = "johndoe_updated"
view_model.email = "invalid"  # Triggers validation error
view_model.email = "john.doe@example.com"
view_model.save_command.execute if view_model.save_command.can_execute?

Shopping Cart with Real-time Updates shows MVVM handling dynamic collections and computed properties.

class Product
  attr_reader :id, :name, :price
  
  def initialize(id, name, price)
    @id = id
    @name = name
    @price = price
  end
end

class CartItem
  attr_accessor :product, :quantity
  
  def initialize(product, quantity = 1)
    @product = product
    @quantity = quantity
  end
  
  def subtotal
    product.price * quantity
  end
end

class ShoppingCart
  attr_reader :items
  
  def initialize
    @items = []
  end
  
  def add_item(product, quantity = 1)
    existing = @items.find { |item| item.product.id == product.id }
    
    if existing
      existing.quantity += quantity
    else
      @items << CartItem.new(product, quantity)
    end
  end
  
  def remove_item(product_id)
    @items.reject! { |item| item.product.id == product_id }
  end
  
  def update_quantity(product_id, quantity)
    item = @items.find { |i| i.product.id == product_id }
    return unless item
    
    if quantity <= 0
      remove_item(product_id)
    else
      item.quantity = quantity
    end
  end
  
  def clear
    @items.clear
  end
end

class CartItemViewModel
  include Observable
  
  attr_reader :product_id, :name, :price, :remove_command
  
  def initialize(cart_item, cart_view_model)
    @cart_item = cart_item
    @cart_view_model = cart_view_model
    @product_id = cart_item.product.id
    @name = cart_item.product.name
    @price = cart_item.product.price
    
    @remove_command = RelayCommand.new(-> { remove })
  end
  
  def quantity
    @cart_item.quantity
  end
  
  def quantity=(value)
    @cart_item.quantity = value
    @cart_view_model.recalculate_totals
    notify_observers(:quantity)
  end
  
  def subtotal
    @cart_item.subtotal
  end
  
  def formatted_price
    "$#{'%.2f' % price}"
  end
  
  def formatted_subtotal
    "$#{'%.2f' % subtotal}"
  end
  
  private
  
  def remove
    @cart_view_model.remove_item(product_id)
  end
end

class ShoppingCartViewModel
  include Observable
  
  attr_reader :items, :checkout_command, :clear_command
  
  def initialize(cart)
    @cart = cart
    @items = []
    @discount_code = nil
    @discount_amount = 0
    
    reload_items
    
    @checkout_command = RelayCommand.new(
      -> { checkout },
      -> { can_checkout? }
    )
    
    @clear_command = RelayCommand.new(
      -> { clear },
      -> { !items.empty? }
    )
  end
  
  def add_product(product, quantity = 1)
    @cart.add_item(product, quantity)
    reload_items
    recalculate_totals
  end
  
  def remove_item(product_id)
    @cart.remove_item(product_id)
    reload_items
    recalculate_totals
  end
  
  def apply_discount(code)
    @discount_code = code
    @discount_amount = calculate_discount(code)
    recalculate_totals
    notify_observers(:discount_applied)
  end
  
  def subtotal
    @items.sum(&:subtotal)
  end
  
  def discount
    @discount_amount
  end
  
  def tax
    (subtotal - discount) * 0.08
  end
  
  def total
    subtotal - discount + tax
  end
  
  def formatted_subtotal
    "$#{'%.2f' % subtotal}"
  end
  
  def formatted_discount
    @discount_amount > 0 ? "-$#{'%.2f' % discount}" : "$0.00"
  end
  
  def formatted_tax
    "$#{'%.2f' % tax}"
  end
  
  def formatted_total
    "$#{'%.2f' % total}"
  end
  
  def item_count
    @items.sum(&:quantity)
  end
  
  def recalculate_totals
    notify_observers(:totals_changed)
    @checkout_command.notify_can_execute_changed
    @clear_command.notify_can_execute_changed
  end
  
  private
  
  def reload_items
    @items = @cart.items.map { |item| CartItemViewModel.new(item, self) }
    notify_observers(:items_changed)
  end
  
  def can_checkout?
    !@items.empty?
  end
  
  def checkout
    notify_observers(:checkout_requested)
  end
  
  def clear
    @cart.clear
    reload_items
    @discount_code = nil
    @discount_amount = 0
    recalculate_totals
  end
  
  def calculate_discount(code)
    case code
    when "SAVE10"
      subtotal * 0.10
    when "SAVE20"
      subtotal * 0.20
    else
      0
    end
  end
end

# Usage
cart = ShoppingCart.new
view_model = ShoppingCartViewModel.new(cart)

view_model.add_observer do |property|
  case property
  when :items_changed
    puts "\nCart Items:"
    view_model.items.each do |item|
      puts "  #{item.name} x#{item.quantity} = #{item.formatted_subtotal}"
    end
  when :totals_changed
    puts "\nSubtotal: #{view_model.formatted_subtotal}"
    puts "Discount: #{view_model.formatted_discount}"
    puts "Tax: #{view_model.formatted_tax}"
    puts "Total: #{view_model.formatted_total}"
  when :discount_applied
    puts "Discount code applied!"
  when :checkout_requested
    puts "Proceeding to checkout..."
  end
end

# Simulate user interactions
product1 = Product.new(1, "Ruby Book", 49.99)
product2 = Product.new(2, "Rails Guide", 39.99)

view_model.add_product(product1, 2)
view_model.add_product(product2, 1)
view_model.apply_discount("SAVE10")
view_model.checkout_command.execute

Form Validation and Submission demonstrates comprehensive validation with multiple fields and async operations.

require 'net/http'
require 'json'

class RegistrationForm
  attr_accessor :username, :email, :password, :password_confirmation, :agree_to_terms
  
  def initialize
    @username = ""
    @email = ""
    @password = ""
    @password_confirmation = ""
    @agree_to_terms = false
  end
  
  def submit
    # Simulated API call
    sleep(1)
    { success: true, user_id: 123 }
  end
end

class AsyncCommand
  def initialize(execute_proc, can_execute_proc = nil)
    @execute_proc = execute_proc
    @can_execute_proc = can_execute_proc
    @executing = false
    @observers = []
  end
  
  def execute
    return if @executing || !can_execute?
    
    @executing = true
    notify_state_changed
    
    Thread.new do
      begin
        @execute_proc.call
      ensure
        @executing = false
        notify_state_changed
      end
    end
  end
  
  def can_execute?
    return false if @executing
    return true unless @can_execute_proc
    @can_execute_proc.call
  end
  
  def executing?
    @executing
  end
  
  def add_observer(&block)
    @observers << block
  end
  
  private
  
  def notify_state_changed
    @observers.each(&:call)
  end
end

class RegistrationViewModel
  include Observable
  
  attr_reader :submit_command, :form
  
  def initialize(form = RegistrationForm.new)
    @form = form
    @validation_errors = {}
    
    @submit_command = AsyncCommand.new(
      -> { submit },
      -> { can_submit? }
    )
    
    @submit_command.add_observer do
      notify_observers(:submitting_changed)
    end
  end
  
  def username
    @form.username
  end
  
  def username=(value)
    @form.username = value
    validate_username
    update_submit_state
  end
  
  def email
    @form.email
  end
  
  def email=(value)
    @form.email = value
    validate_email
    update_submit_state
  end
  
  def password
    @form.password
  end
  
  def password=(value)
    @form.password = value
    validate_password
    validate_password_confirmation if @form.password_confirmation.length > 0
    update_submit_state
  end
  
  def password_confirmation
    @form.password_confirmation
  end
  
  def password_confirmation=(value)
    @form.password_confirmation = value
    validate_password_confirmation
    update_submit_state
  end
  
  def agree_to_terms
    @form.agree_to_terms
  end
  
  def agree_to_terms=(value)
    @form.agree_to_terms = value
    update_submit_state
    notify_observers(:agree_to_terms)
  end
  
  def username_error
    @validation_errors[:username]
  end
  
  def email_error
    @validation_errors[:email]
  end
  
  def password_error
    @validation_errors[:password]
  end
  
  def password_confirmation_error
    @validation_errors[:password_confirmation]
  end
  
  def has_errors?
    !@validation_errors.empty?
  end
  
  def submitting?
    @submit_command.executing?
  end
  
  private
  
  def can_submit?
    !has_errors? && 
    @form.username.length > 0 &&
    @form.email.length > 0 &&
    @form.password.length > 0 &&
    @form.password_confirmation.length > 0 &&
    @form.agree_to_terms
  end
  
  def submit
    result = @form.submit
    
    if result[:success]
      notify_observers(:submission_successful)
    else
      notify_observers(:submission_failed)
    end
  end
  
  def update_submit_state
    notify_observers(:validation_changed)
  end
  
  def validate_username
    @validation_errors.delete(:username)
    
    if @form.username.length < 3
      @validation_errors[:username] = "Username must be at least 3 characters"
    elsif @form.username.length > 20
      @validation_errors[:username] = "Username must be less than 20 characters"
    elsif !@form.username.match?(/\A[a-zA-Z0-9_]+\z/)
      @validation_errors[:username] = "Username can only contain letters, numbers, and underscores"
    end
    
    notify_observers(:username_error)
  end
  
  def validate_email
    @validation_errors.delete(:email)
    
    unless @form.email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
      @validation_errors[:email] = "Email format is invalid"
    end
    
    notify_observers(:email_error)
  end
  
  def validate_password
    @validation_errors.delete(:password)
    
    if @form.password.length < 8
      @validation_errors[:password] = "Password must be at least 8 characters"
    elsif !@form.password.match?(/[A-Z]/)
      @validation_errors[:password] = "Password must contain at least one uppercase letter"
    elsif !@form.password.match?(/[0-9]/)
      @validation_errors[:password] = "Password must contain at least one number"
    end
    
    notify_observers(:password_error)
  end
  
  def validate_password_confirmation
    @validation_errors.delete(:password_confirmation)
    
    if @form.password != @form.password_confirmation
      @validation_errors[:password_confirmation] = "Passwords do not match"
    end
    
    notify_observers(:password_confirmation_error)
  end
end

# Console simulation
view_model = RegistrationViewModel.new

view_model.add_observer do |property|
  case property
  when :username_error
    puts "Username error: #{view_model.username_error}" if view_model.username_error
  when :email_error
    puts "Email error: #{view_model.email_error}" if view_model.email_error
  when :password_error
    puts "Password error: #{view_model.password_error}" if view_model.password_error
  when :password_confirmation_error
    puts "Password confirmation error: #{view_model.password_confirmation_error}" if view_model.password_confirmation_error
  when :submitting_changed
    if view_model.submitting?
      puts "Submitting registration..."
    else
      puts "Submission complete"
    end
  when :submission_successful
    puts "Registration successful!"
  end
end

# Simulate user input
view_model.username = "ab"  # Too short
view_model.username = "newuser123"
view_model.email = "invalid"  # Invalid format
view_model.email = "user@example.com"
view_model.password = "weak"  # Too short
view_model.password = "StrongPass123"
view_model.password_confirmation = "different"  # Doesn't match
view_model.password_confirmation = "StrongPass123"
view_model.agree_to_terms = true
view_model.submit_command.execute

Reference

Component Responsibilities

Component Responsibilities Contains Does Not Contain
Model Domain data and business logic Data structures, validation rules, business rules, persistence logic UI logic, presentation formatting, view state
View UI presentation and user input capture UI markup, visual styling, input controls, basic UI event routing Business logic, data formatting, navigation logic
ViewModel Presentation logic and state management View state, data formatting, input validation, command logic, property notifications UI framework code, direct view references, business rules

Pattern Comparison

Aspect MVVM MVC MVP
View-Controller coupling Loose via data binding Tight via direct calls Medium via interface
Testability High - no UI dependencies Medium - controller tests need mocking High - presenter fully testable
Data flow Bidirectional binding Controller to View Presenter to View via interface
View intelligence Minimal declarative binding Varies by implementation Passive - presenter driven
Platform requirements Data binding support needed Any platform Any platform
Learning curve Moderate - binding concepts Low - familiar pattern Low - straightforward
Best for Complex UIs, frequent changes Server-rendered apps Apps without binding support

Observable Property Implementation Checklist

Step Description Implementation
1 Create observable module or base class Include Observable module or inherit from base
2 Define observable properties Use observable_attr or custom setters
3 Implement observer registration add_observer method accepting blocks
4 Implement notification mechanism notify_observers called on property changes
5 Track property dependencies Document computed properties and their dependencies
6 Notify dependent properties Trigger notifications for computed properties

Command Pattern Checklist

Element Purpose Required Methods
Execute action Perform the command action execute
Can execute predicate Determine if command can run can_execute?
State change notification Notify when availability changes notify_can_execute_changed
Observer registration Register for state changes observe_can_execute_changed

ViewModel Design Checklist

Consideration Questions to Ask Decision Criteria
Property exposure What data does the View need? Expose only data required for presentation
Command definition What user actions are possible? One command per logical user action
Validation location Where should validation occur? ViewModel for presentation rules, Model for business rules
State management What UI state needs tracking? Track editing state, loading state, selection state
Computed properties What derived values are needed? Create computed properties for formatted or aggregated data
Dependencies What services does ViewModel need? Inject all dependencies through constructor

Data Binding Types

Binding Type Direction Use Case Example
One-way to View ViewModel to View Display-only data Status messages, computed values
One-way to ViewModel View to ViewModel Action triggers Button clicks, menu selections
Two-way Bidirectional Editable data Text inputs, checkboxes, sliders
One-time Initial only Static data Configuration values, constants

Common ViewModel Properties

Property Type Example Purpose
Formatted data formatted_date, display_price Present data in UI format
Computed values full_name, total_price Aggregate or calculate from other properties
Validation errors email_error, password_error Display field-specific errors
State flags is_loading, is_editing, has_changes Control UI behavior
Collections items, selected_items Manage lists and selections
Commands save_command, delete_command Encapsulate user actions

Testing Strategy

Test Type Target Tools Coverage
ViewModel unit tests Individual ViewModels RSpec, Minitest Property changes, command execution, validation logic
Model unit tests Domain logic RSpec, Minitest Business rules, data validation, persistence
Integration tests ViewModel-Model interaction RSpec with test doubles Data flow, repository calls, state transitions
UI tests View-ViewModel binding Framework-specific Binding correctness, user interactions

Anti-Patterns to Avoid

Anti-Pattern Description Solution
Fat ViewModel Business logic in ViewModel Move business rules to Model layer
View-dependent ViewModel ViewModel imports View classes Use observer pattern and dependency injection
Anemic ViewModel ViewModel as simple data holder Add presentation logic and commands
Direct Model manipulation in View View modifies Models directly Route all changes through ViewModel
Global state in ViewModel Static or singleton ViewModels Create instance per use, inject dependencies
Missing validation No input validation in ViewModel Validate presentation rules in ViewModel
Circular dependencies ViewModel references View, View references ViewModel One-way dependency from View to ViewModel only