MAUI Lab C# / .NET

ENTRENAMIENTO PARA UN EXAMEN PRÁCTICO

Diagnostica.
Explica. Corrige.

16 escenarios de MAUI y NotasApp. Lee el caso, predice qué ocurrirá y escribe tu solución antes de consultar la propuesta. Los fragmentos ilustran el concepto; requieren el proyecto, paquetes, imports y configuración de plataforma correspondientes.

Cómo resolver cada caso

  1. Describe el síntoma y la causa probable.
  2. Elige un diagnóstico y explica qué evidencia lo apoya.
  3. Propón el cambio mínimo y cómo lo comprobarías.
  4. Contrasta la solución y responde la variación del caso.

16 escenarios

CASO 01 · MVVM

El saludo cambia en memoria, pero no en pantalla

El botón cambia Saludo. El Label sigue mostrando el valor anterior y no aparece ninguna excepción.

Código que debes analizar

public class SaludoViewModel
{
    public string Saludo { get; set; } = "Hola";
    public void Saludar() => Saludo = "Hola, Luis";
}
// Label: Text="{Binding Saludo}"

Decide antes de mirar

Ver solución y respuesta modelo

Qué resuelve

OneWay sí permite actualizar el destino cuando la fuente notifica. Una propiedad automática no avisa por sí sola. El Toolkit genera la notificación y separa la acción de los controles.

Una propuesta de corrección

// Requiere CommunityToolkit.Mvvm
public partial class SaludoViewModel : ObservableObject
{
    [ObservableProperty] private string saludo = "Hola";
    [RelayCommand]
    private void Saludar() => Saludo = "Hola, Luis";
}
// Label: Text="{Binding Saludo}"
// Button: Command="{Binding SaludarCommand}"

Cómo abordarlo

  1. Comprobar BindingContext y nombre de propiedad.
  2. Implementar notificación o usar ObservableProperty.
  3. Enlazar el comando sin manipular el Label.

Ahora cambia el escenario

¿Qué cambiaría si faltara BindingContext en vez de la notificación?

Ver respuesta de la variación

Sin BindingContext, el enlace no encuentra la fuente. Asignaría el ViewModel como contexto y comprobaría el nombre de Saludo; solo después revisaría su notificación.

CASO 02 · MVVM

El formulario no devuelve lo escrito

Un Entry muestra el valor inicial. Al guardar, el modelo todavía tiene el valor antiguo.

Código que debes analizar

<Entry Text="{Binding Nombre, Mode=OneWay}" />

Decide antes de mirar

Ver solución y respuesta modelo

Qué resuelve

OneWay solo comunica fuente→UI. Para edición necesitas el camino inverso y una propiedad escribible; notificación también importa para cambios posteriores desde el modelo.

Una propuesta de corrección

<Entry Text="{Binding Nombre, Mode=TwoWay}" />
// En el ViewModel:
[ObservableProperty] private string nombre = "";

Cómo abordarlo

  1. Identificar la dirección esperada del dato.
  2. Comprobar setter y BindingContext.
  3. Usar TwoWay para entrada editable.

Ahora cambia el escenario

¿Por qué un Label normalmente no necesita TwoWay?

Ver respuesta de la variación

Label normalmente muestra información y no la edita. OneWay es suficiente para recibir cambios notificados desde la fuente; no hay un valor escrito por el usuario que devolver.

CASO 03 · Colecciones

La fila no refleja una edición

Agregar y quitar notas funciona. Cambiar el texto de una nota no actualiza la fila.

Código que debes analizar

public class Nota { public string Texto { get; set; } }
public ObservableCollection<Nota> Notas { get; } = new();
// Más tarde:
Notas[0].Texto = "Texto corregido";

Decide antes de mirar

Ver solución y respuesta modelo

Qué resuelve

CollectionChanged informa altas, bajas y reemplazos. Cambios internos del elemento requieren PropertyChanged en ese objeto. Esto es distinto de persistir el cambio.

Una propuesta de corrección

public partial class Nota : ObservableObject
{
    [ObservableProperty] private string texto = "";
}
// La colección permanece igual.
Notas[0].Texto = "Texto corregido";

Cómo abordarlo

  1. Distinguir cambio de colección de cambio de elemento.
  2. Hacer observable el modelo editable.
  3. Guardar aparte si también debe persistir.

Ahora cambia el escenario

¿Qué ocurriría al reemplazar el elemento completo en vez de editar su Texto?

Ver respuesta de la variación

Reemplazar Notas[0] emite un cambio de colección y permite mostrar el nuevo elemento. Si después se editan propiedades del elemento, ese objeto todavía necesita notificar esos cambios.

CASO 04 · Async

La pantalla se congela al sincronizar

Al tocar Sincronizar, la UI deja de responder mientras espera la red.

Código que debes analizar

private void Sincronizar()
{
    string json = _http.GetStringAsync("posts").Result;
    Mensaje = json;
}

Decide antes de mirar

Ver solución y respuesta modelo

Qué resuelve

Result bloquea el hilo llamador. await permite suspender la espera. No garantiza que un bucle pesado de CPU no bloquee ni reemplaza el manejo de errores.

Una propuesta de corrección

[RelayCommand]
private async Task SincronizarAsync()
{
    try
    {
        Mensaje = await _http.GetStringAsync("posts");
    }
    catch (HttpRequestException)
    {
        Mensaje = "No se pudo contactar el servidor.";
    }
}

Cómo abordarlo

  1. Localizar Result, Wait o trabajo síncrono largo.
  2. Propagar await hasta el comando.
  3. Manejar errores y cancelación según el flujo.

Ahora cambia el escenario

Si el problema fuera calcular millones de elementos, ¿bastaría con poner async?

Ver respuesta de la variación

No. async no mueve automáticamente el cálculo a otro hilo. Para trabajo costoso de CPU evaluaría Task.Run, cancelación y límites; mantendría las actualizaciones de UI en su contexto adecuado.

CASO 05 · Async

El botón queda bloqueado después del error

La API falla y IsBusy queda true. El siguiente toque sale inmediatamente.

Código que debes analizar

if (IsBusy) return;
IsBusy = true;
await _api.ObtenerNotasAsync();
IsBusy = false;

Decide antes de mirar

Ver solución y respuesta modelo

Qué resuelve

Una excepción evita llegar a la última línea. finally restaura el estado tanto en éxito como en error. El bloqueo de doble toque y la política de reintento son responsabilidades complementarias.

Una propuesta de corrección

if (IsBusy) return;
IsBusy = true;
try { await _api.ObtenerNotasAsync(); }
catch (HttpRequestException) { Mensaje = "Reintenta más tarde."; }
finally { IsBusy = false; }

Cómo abordarlo

  1. Identificar qué salida evita limpiar el estado.
  2. Usar finally para restaurarlo.
  3. Informar el error sin perder datos locales.

Ahora cambia el escenario

¿Cómo usarías IsRunning de un comando asíncrono del Toolkit?

Ver respuesta de la variación

Enlazaría el indicador de actividad a IsRunning del comando asíncrono. Revisaría su política de ejecuciones concurrentes y CanExecute; seguiría manejando errores y cancelación en la operación.

CASO 06 · SQLite

La primera consulta dice que no existe la tabla

El constructor inicia CreateTableAsync. La pantalla consulta inmediatamente y falla de forma intermitente.

Código que debes analizar

public NotaRepository()
{
    _db = new SQLiteAsyncConnection(ruta);
    _db.CreateTableAsync<Nota>(); // tarea ignorada
}
public Task<List<Nota>> LeerAsync()
    => _db.Table<Nota>().ToListAsync();

Decide antes de mirar

Ver solución y respuesta modelo

Qué resuelve

Iniciar la creación no significa haberla terminado. Espera una tarea compartida de inicialización antes de cada operación; así varias consultas no lanzan inicializaciones independientes.

Una propuesta de corrección

private readonly SQLiteAsyncConnection _db;
private readonly Task _ready;
public NotaRepository(string ruta)
{
    _db = new SQLiteAsyncConnection(ruta);
    _ready = _db.CreateTableAsync<Nota>();
}
public async Task<List<Nota>> LeerAsync()
{
    await _ready;
    return await _db.Table<Nota>().ToListAsync();
}

Cómo abordarlo

  1. Guardar y observar la tarea de inicialización.
  2. Esperarla antes de acceder a la tabla.
  3. Manejar un fallo de inicialización sin informar éxito.

Ahora cambia el escenario

¿Por qué Task es útil aquí aunque el constructor no pueda ser async?

Ver respuesta de la variación

Un constructor no puede usar await directamente. Puede guardar una Task de inicialización para que los métodos posteriores la esperen, o se puede usar una fábrica asíncrona que entregue el objeto ya preparado.

CASO 07 · Persistencia

La nota desaparece al volver

El usuario escribe y la app guarda únicamente al intentar cerrarse. El proceso termina abruptamente.

Código que debes analizar

string borrador = entrada.Text;
// Se espera a Destroying para guardarlo.

Decide antes de mirar

Ver solución y respuesta modelo

Qué resuelve

Los eventos de ciclo de vida son útiles, pero no una garantía de completar trabajo antes de cada terminación. Guarda al confirmar y, si procede, con autosave controlado; reconstruye al iniciar.

Una propuesta de corrección

[RelayCommand]
private async Task GuardarAsync()
{
    await _repo.GuardarAsync(new Nota { Texto = Texto });
    Mensaje = "Nota guardada";
}
// Al iniciar: await _repo.ObtenerTodasAsync();

Cómo abordarlo

  1. Distinguir memoria de persistencia.
  2. Definir puntos de guardado oportuno.
  3. Esperar y confirmar el resultado antes de mostrar Guardado.

Ahora cambia el escenario

¿Qué límite tendría guardar solo en Preferences un conjunto grande de notas?

Ver respuesta de la variación

Preferences sirve para ajustes pequeños de clave/valor, no como sustituto de una base estructurada grande. Usaría SQLite y un repositorio para notas, consultas y actualizaciones.

CASO 08 · Shell

El detalle abre la nota equivocada

La página se reutiliza y conserva datos anteriores. Cambiar el id recibido no vuelve a cargar la nota.

Código que debes analizar

public int Id { get; set; }
// La nota se carga solo una vez en el constructor.

Decide antes de mirar

Ver solución y respuesta modelo

Qué resuelve

La ruta identifica el registro. Debes reaccionar al parámetro, validar tipo y cargar el dato actualizado. IQueryAttributable ofrece recepción explícita; evita ocultar errores de carga en async void.

Una propuesta de corrección

// Fragmento del ViewModel:
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
    if (query.TryGetValue("id", out var raw) && raw is int id)
        Id = id;
}
// La página espera la carga al aparecer:
protected override async void OnAppearing()
{
    base.OnAppearing();
    try { await _vm.CargarAsync(); }
    catch (Exception) { await DisplayAlertAsync("Error", "No se pudo cargar", "OK"); }
}
// Navegación:
await Shell.Current.GoToAsync("detalle",
    new ShellNavigationQueryParameters { ["id"] = nota.Id });

Cómo abordarlo

  1. Registrar la ruta y validar el parámetro.
  2. Asignar el id al destino.
  3. Esperar carga y manejar ausencia o error.

Ahora cambia el escenario

Si navegaras con una cadena de consulta, ¿cómo validarías y convertirías el id?

Ver respuesta de la variación

Leer el valor recibido, convertirlo con int.TryParse, rechazar IDs inválidos y cargar el registro correspondiente. Para valores de texto construidos en una URI, codificaría los componentes; nunca asumiría que cualquier parámetro es válido.

CASO 09 · Permisos

La cámara no abre después de negar

La app solicita repetidamente permiso y no permite seguir sin foto.

Código que debes analizar

while (await Permissions.RequestAsync<Permissions.Camera>()
       != PermissionStatus.Granted) { }

Decide antes de mirar

Ver solución y respuesta modelo

Qué resuelve

No fuerces un permiso. Consulta estado, solicita desde un contexto válido y permite alternativa. ShouldShowRationale=false no basta por sí solo para inferir bloqueo permanente.

Una propuesta de corrección

var status = await Permissions.CheckStatusAsync<Permissions.Camera>();
if (status != PermissionStatus.Granted)
    status = await Permissions.RequestAsync<Permissions.Camera>();
if (status != PermissionStatus.Granted)
{
    Mensaje = "Puedes guardar la nota sin foto.";
    return;
}
// Ofrecer Ajustes solo si procede y el usuario lo elige.

Cómo abordarlo

  1. Declarar uso en cada plataforma.
  2. Solicitar al usar la función, no en bucle.
  3. Ofrecer alternativa y Ajustes cuando proceda.

Ahora cambia el escenario

¿Por qué no se debe solicitar cámara para guardar una nota de texto?

Ver respuesta de la variación

Guardar texto no necesita cámara. Solicitaría el permiso solo cuando el usuario elija adjuntar una foto; si lo niega, la nota seguiría siendo utilizable.

CASO 10 · Capacidades

Cancelar una foto rompe el formulario

El usuario cierra la cámara. El código intenta leer FileName de un resultado nulo.

Código que debes analizar

var foto = await MediaPicker.Default.CapturePhotoAsync();
var ruta = Path.Combine(FileSystem.AppDataDirectory, foto.FileName);

Decide antes de mirar

Ver solución y respuesta modelo

Qué resuelve

Capturar no garantiza un archivo: cancelación puede devolver null y hay excepciones de permisos/dispositivo. Copia el contenido al almacenamiento privado si debe conservarse.

Una propuesta de corrección

if (!MediaPicker.Default.IsCaptureSupported) return;
// Previamente: gestionar permisos según plataforma.
var foto = await MediaPicker.Default.CapturePhotoAsync();
if (foto is null) return;
var ruta = Path.Combine(FileSystem.AppDataDirectory, foto.FileName);
using var origen = await foto.OpenReadAsync();
using var destino = File.Create(ruta);
await origen.CopyToAsync(destino);

Cómo abordarlo

  1. Separar captura disponible de permiso concedido.
  2. Manejar cancelación y fallos en el llamador.
  3. Copiar la foto con política de nombres y limpieza.

Ahora cambia el escenario

¿Qué harías para evitar sobrescribir dos fotos con el mismo nombre?

Ver respuesta de la variación

Generaría un nombre propio único, por ejemplo un GUID y una extensión permitida, y guardaría esa ruta en el modelo. También definiría limpieza de archivos huérfanos y trataría fallos de copia.

CASO 11 · Conectividad

Hay WiFi, pero la API falla

NetworkAccess indica Internet. El servidor responde500 y la app elimina la nota pendiente como si se hubiera sincronizado.

Código que debes analizar

if (Connectivity.Current.NetworkAccess == NetworkAccess.Internet)
{
    await _http.PostAsJsonAsync("notas", nota);
    nota.Pendiente = false;
}

Decide antes de mirar

Ver solución y respuesta modelo

Qué resuelve

Una respuesta HTTP de error no siempre lanza excepción en Send/Post. Comprueba status y contrato de la API. Si falla, conserva el pendiente y evita reintentos que creen duplicados.

Una propuesta de corrección

using var respuesta = await _http.PostAsJsonAsync("notas", dto, ct);
respuesta.EnsureSuccessStatusCode();
// Validar confirmación/ID devuelto según contrato del servidor.
nota.Pendiente = false;
await _repo.GuardarAsync(nota);

Cómo abordarlo

  1. Guardar local antes de depender de red.
  2. Comprobar status y confirmación.
  3. Conservar pendiente ante fallo y planificar reintento idempotente.

Ahora cambia el escenario

¿Qué ocurre si el servidor guardó, pero se perdió su respuesta?

Ver respuesta de la variación

El cliente no sabe si la operación llegó a completarse. Usaría una identidad estable o clave de idempotencia aceptada por el servidor y reconciliación antes de repetir, para evitar registros duplicados.

CASO 12 · API y JSON

El JSON es válido, pero el objeto no coincide

La API usa title y body. El DTO espera otros nombres sin un mapeo explícito.

Código que debes analizar

public record NotaDto(string Titulo, string Cuerpo);
// Respuesta: { "title":"Compra", "body":"Leche" }

Decide antes de mirar

Ver solución y respuesta modelo

Qué resuelve

Validez sintáctica JSON no garantiza correspondencia semántica. Define DTO acorde al contrato o atributos JsonPropertyName, valida requeridos y transforma al modelo local.

Una propuesta de corrección

public record NotaDto(
    [property: JsonPropertyName("title")] string Titulo,
    [property: JsonPropertyName("body")] string Cuerpo);
// Requiere System.Text.Json.Serialization.
// Validar contenido recibido antes de persistir.

Cómo abordarlo

  1. Inspeccionar nombres y tipos reales del payload.
  2. Definir DTO y mapeo explícito.
  3. Validar y convertir al modelo de la app.

Ahora cambia el escenario

¿Cómo tratarías un body ausente o null?

Ver respuesta de la variación

Declararía el campo como anulable si el contrato permite ausencia, validaría el DTO después de deserializar y decidiría si rechazar el dato o aplicar un valor predeterminado documentado. No guardaría datos incompletos silenciosamente.

CASO 13 · Dependencias

El detalle conserva estado de otra nota

Una página de detalle y su ViewModel se registran Singleton y guardan datos de la nota anterior.

Código que debes analizar

builder.Services.AddSingleton<DetalleViewModel>();
builder.Services.AddSingleton<DetallePage>();

Decide antes de mirar

Ver solución y respuesta modelo

Qué resuelve

Singleton comparte instancia y estado por contenedor. Para detalle independiente puede convenir Transient;no elimina la necesidad de recibir parámetros y recargar correctamente. Elige vida por necesidades, no por una regla única.

Una propuesta de corrección

builder.Services.AddSingleton<NotaRepository>();
builder.Services.AddTransient<DetalleViewModel>();
builder.Services.AddTransient<DetallePage>();
public DetallePage(DetalleViewModel vm)
{
    InitializeComponent();
    BindingContext = vm;
}

Cómo abordarlo

  1. Identificar estado que debe o no compartirse.
  2. Elegir vida apropiada y dependencias.
  3. Cargar y limpiar el detalle al navegar.

Ahora cambia el escenario

¿Cuándo sí sería útil compartir un ViewModel Singleton?

Ver respuesta de la variación

Un Singleton puede servir para estado compartido de sesión o una lista que debe conservarse mientras vive la app. Evitaría usarlo por defecto para estado exclusivo de una nota y controlaría sus suscripciones.

CASO 14 · Interfaz

La lista casi no ocupa espacio

La fila de la lista se mide como Auto. La pantalla tiene espacio, pero no se reparte como esperaba el diseño.

Código que debes analizar

<Grid RowDefinitions="Auto,Auto,Auto">
    <Label Grid.Row="0" Text="Mis notas" />
    <CollectionView Grid.Row="1" />
    <Button Grid.Row="2" Text="Agregar" />
</Grid>

Decide antes de mirar

Ver solución y respuesta modelo

Qué resuelve

Auto mide contenido;la fila estrella recibe espacio restante dentro de una restricción de tamaño. Comprueba también el contenedor padre y evita envolver una lista virtualizada de manera que pierda sus límites.

Una propuesta de corrección

<Grid RowDefinitions="Auto,*,Auto">
    <Label Grid.Row="0" Text="Mis notas" />
    <CollectionView Grid.Row="1" ItemsSource="{Binding Notas}" />
    <Button Grid.Row="2" Text="Agregar" Command="{Binding AgregarCommand}" />
</Grid>

Cómo abordarlo

  1. Verificar filas e índices.
  2. Dar tamaño flexible a la zona de contenido.
  3. Comprobar restricciones del padre y scroll.

Ahora cambia el escenario

¿Qué cambia si usas dos columnas *,2*?

Ver respuesta de la variación

Las estrellas reparten el espacio disponible en proporción1:2: la segunda columna recibe el doble de la primera, después de considerar otras restricciones y tamaños.

CASO 15 · Recursos

El modo oscuro cambia el fondo, pero el texto desaparece

Se cambia el fondo por tema, pero el texto mantiene un color oscuro fijo.

Código que debes analizar

<Label Text="Mis notas" TextColor="#22282D"
       BackgroundColor="{AppThemeBinding Light=White, Dark=#12161A}" />

Decide antes de mirar

Ver solución y respuesta modelo

Qué resuelve

El tema no corrige automáticamente cualquier valor fijo. Usa recursos coherentes para texto/fondo y revisa contraste, estados y controles completos.

Una propuesta de corrección

<Label Text="Mis notas"
       TextColor="{AppThemeBinding Light=#22282D, Dark=#E6EAED}"
       BackgroundColor="{AppThemeBinding Light=White, Dark=#12161A}" />
// En la app, reutiliza colores del ResourceDictionary.

Cómo abordarlo

  1. Identificar pares fondo/texto.
  2. Definir recursos y valores por tema.
  3. Probar contraste y estados interactivos.

Ahora cambia el escenario

¿Por qué un ResourceDictionary evita repetir colores por toda la app?

Ver respuesta de la variación

Un ResourceDictionary centraliza valores con nombre. Cambiar un color actualiza sus referencias, reduce inconsistencias y permite mantener pares Light/Dark reutilizables.

CASO 16 · C# moderno

La nota no tiene ubicación

Latitud es double?. Se lee Value aunque la nota fue creada sin GPS.

Código que debes analizar

double? latitud = nota.Latitud;
Mostrar(latitud.Value);

Decide antes de mirar

Ver solución y respuesta modelo

Qué resuelve

Nullable representa ausencia legítima. Cero es una coordenada válida, no sinónimo universal de sin ubicación. Usa pattern matching o HasValue y muestra un estado adecuado.

Una propuesta de corrección

if (nota.Latitud is double lat)
    Mostrar(lat);
else
    Mensaje = "Nota sin ubicación.";

Cómo abordarlo

  1. Entender por qué el dato es opcional.
  2. Validar antes de acceder a Value.
  3. Mostrar ausencia sin inventar coordenadas.

Ahora cambia el escenario

¿Cuál es la diferencia entre string? y double? en el sistema de tipos?

Ver respuesta de la variación

double? es Nullable<double>, que permite representar ausencia de un tipo valor. string? es una anotación de nulabilidad para un tipo referencia que habilita advertencias del compilador; no crea un nuevo tipo de string en ejecución.