Tehnična Dokumentacija

.NET MAUI & Blazor Aplikacije

Naloga 7, 8 & 9 - Advancne Informacijske Storitve

.NET MAUI Blazor WebAssembly REST API C# 12 🤖 Gemini AI

📖 Uvod

O projektu

Ta projekt zajema implementacijo dveh sodobnih .NET aplikacij:

  • MAUI Aplikacija - Cross-platform mobilna/namizna aplikacija za upravljanje objav (Posts)
  • Blazor Aplikacija - Spletna aplikacija za upravljanje produktov s podatkovnimi vizualizacijami

Obe aplikaciji uporabljata REST API zaledja za izvajanje vseh CRUD operacij (Create, Read, Update, Delete).

Uporabljene tehnologije

.NET 9.0

Najnovejša verzija .NET platforme

C# 12

Programski jezik za celotno aplikacijo

XAML

Deklarativna UI sintaksa za MAUI

Razor

Komponente za Blazor UI

Chart.js

Knjižnica za vizualizacijo podatkov

Bootstrap 5

CSS framework za odzivno oblikovanje

📱 Naloga 7 - .NET MAUI Aplikacija

Pregled aplikacije

MAUI (Multi-platform App UI) aplikacija je cross-platform rešitev za upravljanje objav preko REST API-ja https://jsonplaceholder.typicode.com/posts. Aplikacija omogoča:

  • ✅ Prikaz seznama objav (Posts)
  • ✅ Dodajanje novih objav
  • ✅ Urejanje obstoječih objav
  • ✅ Brisanje objav
  • ✅ Iskanje po naslovu
  • ✅ Sortiranje (A-Z, Z-A)
  • ✅ Prikaz statusnih sporočil

Arhitektura projekta

MAUI/
├── MainPage.xaml              # Glavna stran z seznamom objav
├── MainPage.xaml.cs           # Code-behind za glavno stran
├── Models/
│   └── Post.cs                # Model entitete Post
├── Services/
│   └── ApiService.cs          # Servis za komunikacijo z API
├── Pages/
│   ├── EditPage.xaml          # Stran za dodajanje/urejanje
│   ├── EditPage.xaml.cs
│   └── AboutPage.xaml         # Informacijska stran
├── Components/
│   ├── StatusMessage.xaml     # Custom komponenta za obvestila
│   └── StatusMessage.xaml.cs
└── MauiProgram.cs             # Konfiguracija aplikacije

1. Model podatkov - Post.cs

Definicija entitete za objave:

public class Post
{
    public int Id { get; set; }
    public int UserId { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }
}

Razlaga: Preprost POCO (Plain Old CLR Object) razred, ki predstavlja objavo. Vsebuje vse potrebne lastnosti za serializacijo/deserializacijo JSON podatkov iz API-ja.

2. API Servis - ApiService.cs

Centralizirana komunikacija z REST API:

🔹 GET - Pridobivanje vseh objav

public async Task<List<Post>> GetPostsAsync()
{
    var response = await _client.GetStringAsync(Url);
    var options = new JsonSerializerOptions
    {
        PropertyNameCaseInsensitive = true
    };
    return JsonSerializer.Deserialize<List<Post>>(response, options);
}

Primer uporabe: Klic await _apiService.GetPostsAsync() vrne seznam prvih 20 objav. Uporablja PropertyNameCaseInsensitive za robustno deserializacijo ne glede na velikost črk v JSON-u.

🔹 POST - Dodajanje nove objave

public async Task<bool> AddPostAsync(Post post)
{
    var json = JsonSerializer.Serialize(post);
    var content = new StringContent(json, Encoding.UTF8, "application/json");
    var response = await _client.PostAsync(Url, content);
    return response.IsSuccessStatusCode;
}

Primer: Serializira objekt v JSON in ga pošlje na API. Vrne true če je HTTP status 2xx (uspeh).

🔹 PUT - Posodobitev objave

public async Task<bool> UpdatePostAsync(Post post)
{
    var json = JsonSerializer.Serialize(post);
    var content = new StringContent(json, Encoding.UTF8, "application/json");
    var response = await _client.PutAsync($"{Url}/{post.Id}", content);
    return response.IsSuccessStatusCode;
}

🔹 DELETE - Brisanje objave

public async Task<bool> DeletePostAsync(int id)
{
    var response = await _client.DeleteAsync($"{Url}/{id}");
    return response.IsSuccessStatusCode;
}

Opomba: Vsi методи se obnaša asinhronno (async/await), kar omogoča ne-blokirajočo UI izvršitev.

3. Glavna stran - MainPage.xaml

🎨 Layout struktura

<Grid RowDefinitions="Auto, *, Auto, Auto" Padding="10" RowSpacing="10">
    <!-- Row 0: StatusMessage + Iskanje + Sortiranje -->
    <!-- Row 1: ListView z objavami -->
    <!-- Row 2: Gumb "Dodaj novo objavo" -->
    <!-- Row 3: Gumb "O aplikaciji" -->
</Grid>

Uporablja Grid Layout s 4 vrsticami. Prva vrstica je Auto (se prilagodi vsebini), druga ima * (zasede preostali prostor), ostale dve sta Auto.

🔍 Iskanje in sortiranje

<Entry Placeholder="Išči po naslovu..." 
       TextChanged="OnSearchChanged" />

<Picker Title="Sortiraj" 
        SelectedIndexChanged="OnSortChanged">
    <Picker.ItemsSource>
        <x:Array Type="{x:Type x:String}">
            <x:String>A-Z</x:String>
            <x:String>Z-A</x:String>
        </x:Array>
    </Picker.ItemsSource>
</Picker>

Eventi:

  • TextChanged - sproži se ob vsaki spremembi besedila
  • SelectedIndexChanged - sproži se ob izbiri opcije

📋 ListView z objavami

<ListView x:Name="PostsListView" 
          ItemSelected="OnPostSelected"
          HasUnevenRows="True">
    <ListView.ItemTemplate>
        <DataTemplate>
            <ViewCell>
                <ViewCell.ContextActions>
                    <MenuItem Text="Izbriši" 
                              IsDestructive="True" 
                              Clicked="OnDeleteClicked" 
                              CommandParameter="{Binding .}"/>
                </ViewCell.ContextActions>
                <StackLayout Padding="10">
                    <Label Text="{Binding Title}" FontAttributes="Bold"/>
                    <Label Text="{Binding Body}" TextColor="Gray"/>
                </StackLayout>
            </ViewCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Ključne funkcionalnosti:

  • ItemSelected event - odpre EditPage za urejanje
  • ContextActions - swipe za brisanje (iOS/Android pattern)
  • CommandParameter="{Binding .}" - poda celoten Post objekt
  • HasUnevenRows - omogoča dinamično višino celic

4. Code-behind logika - MainPage.xaml.cs

🔄 Lifecycle & Inicializacija

public ObservableCollection<Post> Posts { get; set; } = new();
private List<Post> _allPosts = new();

protected override async void OnAppearing()
{
    base.OnAppearing();
    if(Posts.Count == 0)
    {
        await LoadData();
    }
}

ObservableCollection avtomatsko osvežuje UI ob spremembah (Add, Remove). OnAppearing() se kliče vsakič, ko se stran prikaže - tukaj preverimo če so podatki že naloženi.

🔍 Implementacija iskanja

private void OnSearchChanged(object sender, TextChangedEventArgs e)
{
    var term = e.NewTextValue.ToLower();
    if (string.IsNullOrWhiteSpace(term))
    {
        RefreshList(_allPosts);
    }
    else
    {
        var filtered = _allPosts
            .Where(p => p.Title.ToLower().Contains(term))
            .ToList();
        RefreshList(filtered);
    }
}

Pristop: Filtriranje se izvaja lokalno na že naloženih podatkih (_allPosts). Uporablja case-insensitive iskanje (ToLower()). Če je iskalni niz prazen, prikaže vse.

🔀 Sortiranje

private void OnSortChanged(object sender, EventArgs e)
{
    var picker = (Picker)sender;
    int selectedIndex = picker.SelectedIndex;

    var sorted = selectedIndex == 0 
        ? Posts.OrderBy(p => p.Title).ToList()
        : Posts.OrderByDescending(p => p.Title).ToList();

    RefreshList(sorted);
}

🗑️ Brisanje z uporabniško potrditev

private async void OnDeleteClicked(object sender, EventArgs e)
{
    var menuItem = (MenuItem)sender;
    var post = (Post)menuItem.CommandParameter;

    bool success = await _apiService.DeletePostAsync(post.Id);
    if(success)
    {
        Posts.Remove(post);
        _allPosts.Remove(post);
        MyStatusMessage.Show("Uspešno izbrisano!", false);
    }
    else
    {
        MyStatusMessage.Show("Napaka pri brisanju!", true);
    }
}

Flow: 1. Pridobi Post iz CommandParameter
2. Pošlje DELETE zahtevo na API
3. Če uspešno, odstrani iz obeh kolekcij (Posts in _allPosts)
4. Prikaže statusno sporočilo preko custom komponente

➕ Dodajanje/Urejanje

private async void OnAddClicked(object sender, EventArgs e)
{
    var page = new EditPage(null);
    await Navigation.PushAsync(page);
    
    var newPost = await page._completion.Task;
    if (newPost != null)
    {
        bool success = await _apiService.AddPostAsync(newPost);
        if (success)
        {
            newPost.Id = _allPosts.Max(p => p.Id) + 1;
            _allPosts.Add(newPost);
            Posts.Add(newPost);
            MyStatusMessage.Show("Uspešno dodano!", false);
        }
    }
}

private async void OnPostSelected(object sender, SelectedItemChangedEventArgs e)
{
    if (e.SelectedItem == null) return;
    
    var post = (Post)e.SelectedItem;
    var page = new EditPage(post);
    await Navigation.PushAsync(page);
    
    var updatedPost = await page._completion.Task;
    if (updatedPost != null)
    {
        bool success = await _apiService.UpdatePostAsync(updatedPost);
        if (success)
        {
            post.Title = updatedPost.Title;
            post.Body = updatedPost.Body;
            RefreshList(_allPosts);
            MyStatusMessage.Show("Uspešno posodobljeno!", false);
        }
    }
    
    PostsListView.SelectedItem = null;
}

TaskCompletionSource vzorec: EditPage uporablja TaskCompletionSource, kar omogoča "čakanje" na rezultat iz navigirane strani. Ko uporabnik klikne "Shrani" ali "Prekliči", se Task zaključi in glavna stran prejme rezultat.

5. EditPage - Dodajanje/Urejanje objav

📝 XAML UI

<VerticalStackLayout Spacing="15" Padding="20">
    <Label Text="Naslov:" />
    <Entry x:Name="EntryTitle" 
           Placeholder="Vnesi naslov" 
           TextChanged="OnTextChanged"/>

    <Label Text="Vsebina:" />
    <Entry x:Name="EntryBody" 
           Placeholder="Vnesi vsebino" 
           HeightRequest="150" 
           TextChanged="OnTextChanged"/>

    <Label x:Name="ErrorLabel" 
           TextColor="Red" 
           IsVisible="False"/>

    <Button Text="Shrani" Clicked="OnSaveClicked"/>
    <Button Text="Prekliči" Clicked="OnCancelClicked"/>
</VerticalStackLayout>

✅ Validacija v realnem času

private void OnTextChanged(object sender, TextChangedEventArgs e)
{
    if (string.IsNullOrWhiteSpace(e.NewTextValue))
    {
        ErrorLabel.Text = "Polje ne sme biti prazno!";
        ErrorLabel.IsVisible = true;
    }
    else
    {
        ErrorLabel.IsVisible = false;
    }
}

Validacija se izvaja ob vsaki spremembi besedila (TextChanged event). Prikaže opozorilo, če je polje prazno.

💾 Shranjevanje

private async void OnSaveClicked(object sender, EventArgs e)
{
    if(string.IsNullOrWhiteSpace(EntryTitle.Text) || 
       string.IsNullOrWhiteSpace(EntryBody.Text))
    {
        await DisplayAlert("Napaka", "Vsa polja morajo biti izpolnjena!", "OK");
        return;
    }

    _post.Title = EntryTitle.Text;
    _post.Body = EntryBody.Text;

    _completion.SetResult(_post);
    await Navigation.PopAsync();
}

_completion.SetResult(_post) posreduje rezultat nazaj na MainPage. Navigation.PopAsync() zapre trenutno stran in se vrne na prejšnjo.

6. StatusMessage - Custom komponenta

🎨 XAML definicija

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             x:Class="MAUI.Components.StatusMessage"
             IsVisible="False">
    <Border StrokeShape="RoundRectangle 10" Padding="10">
        <Label x:Name="MessageLabel"
               TextColor="White"
               HorizontalOptions="Center"
               VerticalOptions="Center"
               FontAttributes="Bold"/>
    </Border>
</ContentView>

⏱️ Časovno omejena vidnost

public async void Show(string message, bool isError)
{
    MessageLabel.Text = message;
    MessageBorder.BackgroundColor = isError ? Colors.Red : Colors.Green;
    this.IsVisible = true;

    await Task.Delay(3000);
    this.IsVisible = false;
}

Funkcionalnost: Prikaže sporočilo za 3 sekunde, nato se avtomatsko skrije. Barva se dinamično nastavi glede na tip sporočila (rdeča za napako, zelena za uspeh).

✅ Izpolnjene zahteve - MAUI

Zahteva Implementacija Lokacija
Pridobivanje podatkov iz API-ja ✅ ApiService.GetPostsAsync() Services/ApiService.cs
Vse CRUD operacije ✅ GET, POST, PUT, DELETE Services/ApiService.cs
Preverjanje veljavnosti podatkov ✅ TextChanged validacija, DisplayAlert EditPage.xaml.cs
Prikaz sporočil o napakah ✅ StatusMessage komponenta Components/StatusMessage.xaml
Vsaj 1 custom komponenta ✅ StatusMessage Components/
Vsaj 3 različne postavitve ✅ Grid, StackLayout, VerticalStackLayout *.xaml files
Vsaj 5 različnih tipov dogodkov ✅ TextChanged, Clicked, SelectedIndexChanged, ItemSelected, ContextActions MainPage.xaml, EditPage.xaml
Vsaj 2 podstrani ✅ EditPage, AboutPage Pages/
Sortiranje po atributu ✅ Sortiranje A-Z, Z-A po Title MainPage.xaml.cs
Iskanje po atributu ✅ Iskanje po Title MainPage.xaml.cs
Pravilno obravnavanje napak ✅ try-catch, status codes, uporabniška obvestila MainPage.xaml.cs

🎯 Ključne funkcionalnosti

📱 Cross-platform

Deluje na Windows, macOS, iOS, Android

🔄 Real-time validacija

Takojšnje povratne informacije uporabniku

🎨 Custom komponente

Ponovno uporabne StatusMessage komponente

🔍 Iskanje & Sortiranje

Hitro filtriranje lokalnih podatkov

💾 Optimizirana UI

ObservableCollection za avtomatske posodobitve

⚡ Async/Await

Ne-blokajoča asinhronska komunikacija

🌐 Naloga 8 - Blazor WebAssembly Aplikacija

Pregled aplikacije

Blazor WebAssembly aplikacija je spletna single-page aplikacija (SPA) za upravljanje produktov. Uporablja API https://dummyjson.com/products in vključuje napredne funkcionalnosti:

  • ✅ CRUD operacije nad produkti
  • ✅ Iskanje in filtriranje po imenu
  • ✅ Sortiranje po ceni (naraščajoče/padajoče)
  • ✅ 4 različni Chart.js grafi za vizualizacijo
  • ✅ Responsive dizajn (Bootstrap 5)
  • ✅ JavaScript interop
  • ✅ Custom dialog komponenta
  • ✅ Validacija podatkov

Arhitektura projekta

Blazor/
├── Program.cs                 # Konfiguracija aplikacije
├── Pages/
│   ├── Products.razor         # Glavna stran s produkti
│   ├── EditProduct.razor      # Dodajanje/urejanje produkta
│   ├── Home.razor             # Domača stran
│   ├── Counter.razor          # Demo stran
│   └── Weather.razor          # Demo stran
├── Models/
│   └── Product.cs             # Model produkta z validacijo
├── Services/
│   └── ProductService.cs      # Servis za API operacije
├── Shared/
│   ├── ConfirmDialog.razor    # Custom dialog komponenta
│   ├── MainLayout.razor       # Osnovna postavitev
│   └── NavMenu.razor          # Navigacijski meni
└── wwwroot/
    ├── index.html             # Root HTML + Chart.js setup
    └── css/app.css            # Custom CSS styling

1. Model podatkov - Product.cs

public class Product
{
    public int Id { get; set; }

    [Required(ErrorMessage = "Naslov je obvezen")]
    public string Title { get; set; } = string.Empty;

    public string Description { get; set; } = string.Empty;

    [Range(0, 10000, ErrorMessage = "Cena mora biti pozitivna")]
    public decimal Price { get; set; }

    public double Rating { get; set; }
    public int Stock { get; set; }
    public string Category { get; set; } = "General";
    public string Thumbnail { get; set; }
}

Data Annotations:

  • [Required] - polje je obvezno
  • [Range(0, 10000)] - cena mora biti med 0 in 10000
  • Omogoča avtomatsko validacijo v <EditForm> komponentah

2. ProductService - Simulacija API klicev

💡 Lokalna simulacija

Pomembno: Ker DummyJSON API ne omogoča resničnih sprememb podatkov, servis uporablja lokalno simulacijo s statično listo _lokalniProdukti. Prvi klic naloži podatke iz API-ja, nadaljnje operacije se izvajajo lokalno.

📥 GET - Pridobi vse produkte

public async Task<List<Product>> GetProductsAsync()
{
    if (!_soPodatkiNalozeni)
    {
        var response = await _http.GetFromJsonAsync<ProductResponse>("products?limit=10");
        _lokalniProdukti = response.Products;
        _soPodatkiNalozeni = true;
    }

    return _lokalniProdukti;
}

Prvi klic: naloži 10 produktov iz API-ja in jih shrani lokalno.
Nadaljnji klici: vrne lokalno shranjene podatke (brez dodatnih HTTP zahtev).

🔍 GET BY ID

public async Task<Product> GetProductByIdAsync(int id)
{
    var product = _lokalniProdukti.FirstOrDefault(p => p.Id == id);
    return await Task.FromResult(product);
}

➕ ADD - Dodajanje

public async Task AddProductAsync(Product product)
{
    product.Id = _lokalniProdukti.Max(p => p.Id) + 1;
    product.Thumbnail = "https://cdn.dummyjson.com/product-images/1/thumbnail.jpg";
    _lokalniProdukti.Add(product);
    await Task.CompletedTask;
}

Generira nov ID (Max(Id) + 1), nastavi dummy sliko in doda v lokalno listo.

✏️ UPDATE - Posodabljanje

public async Task UpdateProductAsync(int id, Product product)
{
    var obstojeci = _lokalniProdukti.FirstOrDefault(p => p.Id == id);
    if (obstojeci != null)
    {
        obstojeci.Title = product.Title;
        obstojeci.Price = product.Price;
        obstojeci.Stock = product.Stock;
    }
    await Task.CompletedTask;
}

🗑️ DELETE - Brisanje

public async Task DeleteProductAsync(int id)
{
    var item = _lokalniProdukti.FirstOrDefault(p => p.Id == id);
    if (item != null)
    {
        _lokalniProdukti.Remove(item);
    }
    await Task.CompletedTask;
}

3. Products.razor - Glavna stran

🎨 UI struktura

@page "/products"
@using Blazor.Models
@using Blazor.Services
@inject ProductService ProductService
@inject IJSRuntime JS

<h3>Upravljanje Produktov</h3>

<!-- Iskanje + Sortiranje + Dodaj -->
<div class="row mb-3">
    <div class="col-md-4">
        <input @bind="searchTerm" @bind:event="oninput" />
    </div>
    <div class="col-md-4">
        <button @onclick="SortByPrice">
            Sortiraj po ceni: @(isAscending ? "⬆" : "⬇")
        </button>
    </div>
    <div class="col-md-4">
        <a href="/product/add" class="btn btn-success">Dodaj</a>
    </div>
</div>

📊 4 Chart.js grafi

<div class="card mb-4">
    <div class="card-header">Vizualizacija podatkov</div>
    <div class="card-body">
        <div class="row">
            <div class="col-md-3">
                <canvas id="chartPrice"></canvas>
            </div>
            <div class="col-md-3">
                <canvas id="chartStock"></canvas>
            </div>
            <div class="col-md-3">
                <canvas id="chartRating"></canvas>
            </div>
            <div class="col-md-3">
                <canvas id="chartValue"></canvas>
            </div>
        </div>
    </div>
</div>

📋 Tabela s produkti

<table class="table table-striped table-hover">
    <thead class="table-dark">
        <tr>
            <th>Slika</th>
            <th>Naziv</th>
            <th>Kategorija</th>
            <th>Cena</th>
            <th>Zaloga</th>
            <th>Akcije</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var p in FilteredProducts)
        {
            <tr>
                <td>
                    <img src="@p.Thumbnail" alt="@p.Title" 
                         style="width: 50px; height: 50px;"/>
                </td>
                <td><strong>@p.Title</strong></td>
                <td>@p.Category</td>
                <td>@p.Price €</td>
                <td>
                    @if(p.Stock < 10) 
                    { 
                        <span class="text-danger">@p.Stock (Nizka!)</span> 
                    } 
                    else 
                    { 
                        <span>@p.Stock</span> 
                    }
                </td>
                <td>
                    <a href="/product/edit/@p.Id">Uredi</a>
                    <button @onclick="() => RequestDelete(p)">Izbriši</button>
                </td>
            </tr>
        }
    </tbody>
</table>

Pogojno oblikovanje: Če je zaloga (Stock) manjša od 10, se prikaže v rdeči barvi z opozorilom "Nizka!".

4. C# Logika - Products.razor @code

🔍 Filtriranje in sortiranje

private List<Product> products;
private string searchTerm = "";
private bool isAscending = true;

private IEnumerable<Product> FilteredProducts
{
    get
    {
        if (products == null) return new List<Product>();

        var res = products.AsEnumerable();

        // 1. Filtriranje (Iskanje)
        if (!string.IsNullOrWhiteSpace(searchTerm))
        {
            res = res.Where(p => 
                p.Title.Contains(searchTerm, StringComparison.OrdinalIgnoreCase));
        }

        // 2. Sortiranje
        if (isAscending)
            res = res.OrderBy(p => p.Price);
        else
            res = res.OrderByDescending(p => p.Price);

        return res;
    }
}

Computed property: FilteredProducts je dinamična lastnost, ki se avtomatsko preračuna ob spremembi searchTerm ali isAscending. UI se avtomatsko posodobi brez dodatnih klicev.

📊 Chart.js integracija

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender && products != null)
    {
        await GenerateCharts();
    }
}

private async Task GenerateCharts()
{
    var labels = products.Take(10)
        .Select(p => p.Title.Length > 10 
            ? p.Title.Substring(0, 10) + "..." 
            : p.Title)
        .ToArray();
    
    var top10Products = products.Take(10).ToList();

    // 1. Graf Cen (Line Chart)
    await JS.InvokeVoidAsync("setupChart", "chartPrice", new
    {
        type = "line",
        data = new
        {
            labels = labels,
            datasets = new[] { 
                new { 
                    label = "Cena (€)", 
                    data = top10Products.Select(p => p.Price), 
                    borderColor = "blue", 
                    tension = 0.4 
                } 
            }
        }
    });

    // 2. Graf Zaloge (Bar Chart)
    await JS.InvokeVoidAsync("setupChart", "chartStock", new
    {
        type = "bar",
        data = new
        {
            labels = labels,
            datasets = new[] { 
                new { 
                    label = "Zaloga (kos)", 
                    data = top10Products.Select(p => p.Stock), 
                    backgroundColor = "green" 
                } 
            }
        }
    });

    // 3. Graf Ocen (Bar Chart)
    await JS.InvokeVoidAsync("setupChart", "chartRating", new
    {
        type = "bar",
        data = new
        {
            labels = labels,
            datasets = new[] { 
                new { 
                    label = "Ocena (1-5)", 
                    data = top10Products.Select(p => p.Rating), 
                    backgroundColor = "orange" 
                } 
            }
        }
    });

    // 4. Graf Vrednosti zaloge (Line Chart with fill)
    await JS.InvokeVoidAsync("setupChart", "chartValue", new
    {
        type = "line",
        data = new
        {
            labels = labels,
            datasets = new[] { 
                new { 
                    label = "Vrednost zaloge (€)", 
                    data = top10Products.Select(p => p.Price * p.Stock), 
                    borderColor = "purple", 
                    fill = true,
                    backgroundColor = "rgba(128, 0, 128, 0.2)"
                } 
            }
        }
    });
}

JavaScript Interop:

  • OnAfterRenderAsync(firstRender) - kliče se po renderiranju komponente
  • firstRender - zagotavlja, da se grafi narišejo samo enkrat
  • JS.InvokeVoidAsync("setupChart", ...) - kliče JavaScript funkcijo iz C#
  • Podatki se posredujejo kot anonymous object, ki se avtomatsko serializira v JSON

🗑️ Brisanje z dialogom

private bool showDeleteDialog = false;
private Product productToDelete;

private void RequestDelete(Product p)
{
    productToDelete = p;
    showDeleteDialog = true;
}

private async Task HandleDeleteConfirmation(bool confirmed)
{
    showDeleteDialog = false;

    if (confirmed && productToDelete != null)
    {
        await ProductService.DeleteProductAsync(productToDelete.Id);
        products = await ProductService.GetProductsAsync();
        
        await JS.InvokeVoidAsync("showAlert", 
            $"Produkt '{productToDelete.Title}' je bil uspešno izbrisan!");
        
        productToDelete = null;
    }
}

Two-way dialog komunikacija: 1. RequestDelete() prikaže dialog
2. Dialog sproži OnConfirmationChange event
3. HandleDeleteConfirmation() obdela rezultat
4. Če je potrjeno, izbriše produkt in prikaže JS alert

5. EditProduct.razor - Dodajanje/Urejanje

🔀 Dinamična ruta

@page "/product/add"
@page "/product/edit/{Id:int}"

[Parameter] public int Id { get; set; }

Ista komponenta obdeluje dve različni poti:

  • /product/add - Id = 0 (dodajanje)
  • /product/edit/5 - Id = 5 (urejanje)

📝 EditForm z validacijo

<EditForm Model="product" OnValidSubmit="HandleSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <div class="mb-3">
        <label>Naziv</label>
        <InputText class="form-control" @bind-Value="product.Title" />
    </div>
    
    <div class="mb-3">
        <label>Cena</label>
        <InputNumber class="form-control" @bind-Value="product.Price" />
    </div>
    
    <div class="mb-3">
        <label>Zaloga</label>
        <InputNumber class="form-control" @bind-Value="product.Stock" />
    </div>

    <button type="submit">Shrani</button>
    <button type="button" @onclick="Cancel">Prekliči</button>
</EditForm>

Blazor validacija:

  • <DataAnnotationsValidator /> - aktivira validacijo iz [Required], [Range] atributov
  • <ValidationSummary /> - prikaže vse napake na enem mestu
  • OnValidSubmit - sproži se SAMO če je forma veljavna
  • <InputText>, <InputNumber> - Blazor komponente z avtomatsko validacijo

💾 Shranjevanje logike

protected override async Task OnInitializedAsync()
{
    if (Id != 0)
    {
        product = await ProductService.GetProductByIdAsync(Id);
    }
}

private async Task HandleSubmit()
{
    if (Id == 0)
    {
        await ProductService.AddProductAsync(product);
    }
    else
    {
        await ProductService.UpdateProductAsync(Id, product);
    }

    NavManager.NavigateTo("/products");
}

private void Cancel()
{
    NavManager.NavigateTo("/products");
}

OnInitializedAsync() - če Id != 0, naloži obstoječ produkt za urejanje.
HandleSubmit() - glede na Id odloči, ali gre za dodajanje ali posodobitev.
NavigationManager - programatska navigacija na drugo stran.

6. ConfirmDialog.razor - Custom komponenta

🎨 Modal dialog UI

@if (Show)
{
    <div class="modal fade show d-block" 
         style="background-color: rgba(0,0,0,0.5);">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5>Potrditev</h5>
                    <button @onclick="Close">×</button>
                </div>
                <div class="modal-body">
                    <p>@Message</p>
                </div>
                <div class="modal-footer">
                    <button @onclick="Close">Prekliči</button>
                    <button @onclick="Confirm">Potrdi</button>
                </div>
            </div>
        </div>
    </div>
}

🔄 EventCallback komunikacija

@code {
    [Parameter] public bool Show { get; set; }
    [Parameter] public string Message { get; set; } = "Ali ste prepričani?";
    [Parameter] public EventCallback<bool> OnConfirmationChange { get; set; }

    private async Task Close() 
        => await OnConfirmationChange.InvokeAsync(false);
    
    private async Task Confirm() 
        => await OnConfirmationChange.InvokeAsync(true);
}

Parametri:

  • Show - kontrolira vidnost dialoga
  • Message - besedilo za prikaz
  • OnConfirmationChange - callback, ki pošlje rezultat (true/false) nazaj na starševsko komponento

📲 Uporaba komponente

<ConfirmDialog 
    Show="showDeleteDialog" 
    Message="@($"Ali res želite izbrisati produkt '{productToDelete?.Title}'?")"
    OnConfirmationChange="HandleDeleteConfirmation" />

7. JavaScript Interop - index.html

📦 Chart.js setup

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
    window.setupChart = function(canvasId, config) {
        var ctx = document.getElementById(canvasId).getContext('2d');
        
        // Destroy existing chart if exists
        if (window[canvasId + '_chart']) {
            window[canvasId + '_chart'].destroy();
        }
        
        window[canvasId + '_chart'] = new Chart(ctx, config);
    }

    window.showAlert = function(message) {
        alert(message);
    }
</script>

Funkcionalnosti:

  • setupChart() - inicializira Chart.js graf z konfiguracijo
  • Shranjuje graf v window[canvasId + '_chart'] za kasnejše uničenje
  • showAlert() - prikaže preprosto JavaScript opozorilo
  • Te funkcije se kličejo iz C# preko JSRuntime

8. Responsive Design

📱 Bootstrap Grid sistem

<div class="row">
    <div class="col-md-4">Iskanje</div>
    <div class="col-md-4">Sortiranje</div>
    <div class="col-md-4">Dodaj gumb</div>
</div>

<!-- Grafi -->
<div class="row">
    <div class="col-md-3">Graf 1</div>
    <div class="col-md-3">Graf 2</div>
    <div class="col-md-3">Graf 3</div>
    <div class="col-md-3">Graf 4</div>
</div>

Breakpoint system:

  • col-md-4 - na srednje velikih zaslonih (≥768px) zavzame 4/12 (33.33%)
  • col-md-3 - na srednje velikih zaslonih zavzame 3/12 (25%)
  • Na manjših zaslonih se stolpci zložijo vertikalno (100% širine)

📊 Responsive tabela

<div class="table-responsive">
    <table class="table table-striped table-hover">
        <!-- Tabela -->
    </table>
</div>

table-responsive omogoča horizontalno drsenje tabele na majhnih zaslonih.

✅ Izpolnjene zahteve - Blazor

Zahteva Implementacija Lokacija
Pridobivanje podatkov iz API-ja ✅ ProductService.GetProductsAsync() Services/ProductService.cs
CRUD operacije ✅ Add, Update, Delete metode Services/ProductService.cs
Iskanje in filtriranje ✅ FilteredProducts computed property Products.razor
Preverjanje veljavnosti ✅ DataAnnotations + EditForm Product.cs, EditProduct.razor
Prikaz sporočil o napakah ✅ ValidationSummary, JS alerts EditProduct.razor, Products.razor
Navigacija med stranmi ✅ NavigationManager, NavMenu EditProduct.razor, Shared/NavMenu.razor
Odzivna oblika ✅ Bootstrap 5 grid sistem Products.razor, MainLayout.razor
Vizualizacija podatkov ✅ Chart.js integracija Products.razor, index.html
Vsaj 4 grafi ✅ Cena (line), Zaloga (bar), Ocena (bar), Vrednost (line fill) Products.razor
Brez vnašanja ID-jev ✅ Parametri v URL, direktni objekti Products.razor, EditProduct.razor
JavaScript funkcija ✅ setupChart(), showAlert() wwwroot/index.html
Vsaj 1 komponenta ✅ ConfirmDialog Shared/ConfirmDialog.razor

🎯 Ključne funkcionalnosti

🌐 WebAssembly

Deluje v brskalniku brez strežnika

📊 4 različni grafi

Line charts, bar charts z Chart.js

🎨 Bootstrap 5

Responsive design za vse naprave

✅ Validacija

Data Annotations + EditForm

🔧 JS Interop

Integracija Chart.js iz C#

🔄 Reactive UI

Avtomatska posodobitev ob spremembah

🤖 Naloga 9 - Gemini LLM Integracija

Pregled funkcionalnosti

V Blazor aplikacijo je integriran Google Gemini LLM za dinamično generiranje produktnih predlogov. Ključne značilnosti:

  • Dinamičen prompt - sestavlja se glede na uporabniški vnos (kategorija, proračun, ciljna skupina)
  • Strukturiran JSON odgovor - LLM vrača podatke v določenem formatu
  • Deserializacija v C# objekte - avtomatsko pretvorba JSON → C# modeli
  • Real-time validacija - preverjanje vnosov pred klicem API-ja
  • Error handling - obravnavanje napak, mock podatki za testiranje

Arhitektura rešitve

Blazor/
├── Models/
│   └── GeminiModels.cs           # Vsi modeli za Gemini API
│       ├── GeminiRequest         # Request structure
│       ├── GeminiResponse        # Response structure
│       ├── ProductSuggestion     # Domenski model
│       └── ProductSuggestionsResponse
├── Services/
│   └── GeminiService.cs          # Logika za Gemini API klice
└── Pages/
    └── AISuggestions.razor       # UI za AI asistenta

1. Modeli - GeminiModels.cs

🔹 API Request/Response modeli

public class GeminiRequest
{
    [JsonPropertyName("contents")]
    public List<GeminiRequestContent> Contents { get; set; }

    [JsonPropertyName("generationConfig")]
    public GeminiGenerationConfig GenerationConfig { get; set; }
}

public class GeminiResponse
{
    [JsonPropertyName("candidates")]
    public List<GeminiCandidate> Candidates { get; set; }
}

Modeli za komunikacijo z Gemini API. Uporablja [JsonPropertyName] atribute za ujemanje s camelCase poimenovanjem v JSON-u.

🔹 Domenski modeli (strukturiran LLM odgovor)

public class ProductSuggestion
{
    [JsonPropertyName("name")]
    public string Name { get; set; }

    [JsonPropertyName("description")]
    public string Description { get; set; }

    [JsonPropertyName("estimatedPrice")]
    public decimal EstimatedPrice { get; set; }

    [JsonPropertyName("category")]
    public string Category { get; set; }

    [JsonPropertyName("targetAudience")]
    public string TargetAudience { get; set; }
}

public class ProductSuggestionsResponse
{
    [JsonPropertyName("suggestions")]
    public List<ProductSuggestion> Suggestions { get; set; }

    [JsonPropertyName("summary")]
    public string Summary { get; set; }
}

Pomembno: Ti modeli definirajo strukturo, ki jo LLM MORA vrniti. To omogoča programsko obdelavo odgovora namesto parseanja prostega besedila.

2. Gemini Servis - GeminiService.cs

🔧 Konstrukcija dinamičnega prompta

public async Task<ProductSuggestionsResponse> GetProductSuggestionsAsync(
    string category, string budget, string targetAudience)
{
    // DINAMIČEN PROMPT - sestavljen glede na uporabniški vnos
    string dynamicPrompt = $@"
You are a product recommendation assistant. Based on the following user preferences, 
suggest 3 relevant products.

User Preferences:
- Category: {category}
- Budget: {budget} EUR
- Target Audience: {targetAudience}

IMPORTANT: Respond ONLY with valid JSON in this exact format:
{{
    ""suggestions"": [
        {{
            ""name"": ""Product name"",
            ""description"": ""Brief description"",
            ""estimatedPrice"": 50.00,
            ""category"": ""{category}"",
            ""targetAudience"": ""{targetAudience}""
        }}
    ],
    ""summary"": ""Brief summary of suggestions""
}}

Generate 3 product suggestions that fit the budget and target audience.";
    
    // ... pošlji na API
}

Ključni pristop:

  • Prompt se dinamično sestavlja z $@"..." string interpolacijo
  • Vključuje TOČNO strukturo JSON-a, ki jo pričakujemo
  • Navodilo "Respond ONLY with valid JSON" je kritično za zanesljivo parseanje

📡 Klic Gemini API

// Pripravi request
var geminiRequest = new GeminiRequest
{
    Contents = new List<GeminiRequestContent>
    {
        new GeminiRequestContent
        {
            Parts = new List<GeminiRequestPart>
            {
                new GeminiRequestPart { Text = dynamicPrompt }
            }
        }
    },
    GenerationConfig = new GeminiGenerationConfig
    {
        Temperature = 0.7,  // Kreativnost modela (0.0 - 1.0)
        MaxOutputTokens = 1024
    }
};

// Pošlji na API - POMEMBNO: API ključ mora biti v headerju!
var request = new HttpRequestMessage(HttpMethod.Post, BASE_URL);
request.Headers.Add("X-goog-api-key", API_KEY);
request.Content = JsonContent.Create(geminiRequest);

var response = await _httpClient.SendAsync(request);

if (!response.IsSuccessStatusCode)
{
    var errorContent = await response.Content.ReadAsStringAsync();
    throw new Exception($"Gemini API error: {response.StatusCode} - {errorContent}");
}

// Pridobi odgovor
var geminiResponse = await response.Content.ReadFromJsonAsync<GeminiResponse>();
var rawText = geminiResponse?.Candidates?.FirstOrDefault()
    ?.Content?.Parts?.FirstOrDefault()?.Text;

🔄 Parsiranje JSON odgovora

// Odstrani markdown code blocks (če jih LLM doda)
var jsonText = rawText.Trim();
if (jsonText.StartsWith("```json"))
{
    jsonText = jsonText.Substring(7);
}
if (jsonText.EndsWith("```"))
{
    jsonText = jsonText.Substring(0, jsonText.Length - 3);
}
jsonText = jsonText.Trim();

// DESERIALIZACIJA v C# objekte
var options = new JsonSerializerOptions
{
    PropertyNameCaseInsensitive = true
};
var productSuggestions = JsonSerializer.Deserialize<ProductSuggestionsResponse>(
    jsonText, 
    options
);

return productSuggestions;

LLM včasih vrne JSON zavit v markdown code blocks (```json ... ```). Ta koda to odstrani in deserializira JSON v ProductSuggestionsResponse objekt.

3. UI - AISuggestions.razor

📝 Uporabniški vnos

<div class="row">
    <div class="col-md-4">
        <label>Kategorija produkta:</label>
        <select @bind="selectedCategory">
            <option value="Elektronika">Elektronika</option>
            <option value="Pohištvo">Pohištvo</option>
            <!-- ... -->
        </select>
    </div>

    <div class="col-md-4">
        <label>Proračun (EUR):</label>
        <input type="number" @bind="budget" />
    </div>

    <div class="col-md-4">
        <label>Ciljna skupina:</label>
        <select @bind="targetAudience">
            <option value="Otroci">Otroci</option>
            <option value="Odrasli">Odrasli</option>
            <!-- ... -->
        </select>
    </div>
</div>

<button @onclick="GetSuggestions" disabled="@isLoading">
    @if (isLoading)
    {
        <span class="spinner-border"></span> Generiranje...
    }
    else
    {
        🚀 Pridobi AI predloge
    }
</button>

🎨 Prikaz rezultatov

@if (suggestions != null)
{
    <div class="card">
        <div class="card-header">✅ AI generiran povzetek</div>
        <div class="card-body">
            <p>@suggestions.Summary</p>
        </div>
    </div>

    <div class="row">
        @foreach (var suggestion in suggestions.Suggestions)
        {
            <div class="col-md-4">
                <div class="card">
                    <h5>@suggestion.Name</h5>
                    <p>@suggestion.Description</p>
                    <h4 class="text-success">@suggestion.EstimatedPrice €</h4>
                    <span class="badge">@suggestion.Category</span>
                    <span class="badge">@suggestion.TargetAudience</span>
                </div>
            </div>
        }
    </div>

    <!-- Raw JSON za debugging -->
    <pre><code>@rawJsonResponse</code></pre>
}

💻 C# Logika

@code {
    private string selectedCategory = "";
    private int budget = 100;
    private string targetAudience = "";
    private bool isLoading = false;
    private ProductSuggestionsResponse suggestions;

    private async Task GetSuggestions()
    {
        // Validacija
        if (string.IsNullOrEmpty(selectedCategory))
        {
            errorMessage = "Prosim izberi kategorijo!";
            return;
        }

        isLoading = true;

        try
        {
            // KLIC GEMINI API
            suggestions = await GeminiService.GetProductSuggestionsAsync(
                selectedCategory, 
                budget.ToString(), 
                targetAudience
            );

            // Shrani JSON za debugging
            rawJsonResponse = JsonSerializer.Serialize(suggestions, 
                new JsonSerializerOptions { WriteIndented = true });
        }
        catch (Exception ex)
        {
            errorMessage = $"Napaka: {ex.Message}";
        }
        finally
        {
            isLoading = false;
        }
    }
}

4. Nastavitev API ključa

⚠️ POMEMBNO: Za delovanje potrebuješ Gemini API ključ!

📋 Koraki za pridobitev ključa:

  1. Pojdi na Google AI Studio
  2. Klikni na "Get API Key"
  3. Ustvari nov projekt (če še nimate)
  4. Kopiraj API ključ
  5. V GeminiService.cs zamenjaj YOUR_GEMINI_API_KEY_HERE s pravim ključem

🔒 Produkcijska varnost

// ❌ NAROBE - hard-coded ključ v kodi
private const string API_KEY = "Kljucek...";

// ✅ PRAVILNO - iz environment variable ali appsettings.json
private readonly string _apiKey;

public GeminiService(IConfiguration configuration)
{
    _apiKey = configuration["Gemini:ApiKey"];
}

// appsettings.json:
{
  "Gemini": {
    "ApiKey": "YOUR_API_KEY_HERE"
  }
}

V produkciji NIKOLI ne shrani API ključev direktno v kodo! Uporabi konfiguracijsko datoteko ali environment variable, ki niso v verzijskem sistemu (dodaj v .gitignore).

✅ Izpolnjene zahteve - Gemini LLM

Zahteva Implementacija Lokacija
Dinamičen prompt (ne statičen) ✅ Prompt sestavljen iz category, budget, targetAudience GeminiService.cs
Klic na strežniku ✅ HTTP klic v GeminiService (lahko premakneš v WebAPI backend) Services/GeminiService.cs
Prikaz na odjemalcu ✅ Blazor stran z Razor komponentami Pages/AISuggestions.razor
Strukturiran JSON odgovor ✅ LLM vrne JSON z določeno strukturo ProductSuggestionsResponse model
Deserializacija v C# razred ✅ JsonSerializer.Deserialize<ProductSuggestionsResponse> GeminiService.cs

🎯 Testiranje brez API ključa

Servis vključuje mock podatke za testiranje:

private ProductSuggestionsResponse GetMockSuggestions(
    string category, string targetAudience)
{
    return new ProductSuggestionsResponse
    {
        Summary = $"Mock predlogi za '{category}'",
        Suggestions = new List<ProductSuggestion>
        {
            new ProductSuggestion
            {
                Name = $"{category} - Premium",
                Description = "Visokokakovostna opcija",
                EstimatedPrice = 99.99m,
                Category = category,
                TargetAudience = targetAudience
            },
            // ... več predlogov
        }
    };
}

Če API klic ne uspe (napačen ključ, omrežna napaka), se avtomatsko uporabijo mock podatki. To omogoča testiranje UI brez delujoče AI integracije.

🚀 Primer delovanja

1️⃣ Uporabnik vnese

Kategorija: "Elektronika"
Proračun: 500 EUR
Skupina: "Profesionalci"

2️⃣ Sestavljen prompt

"Suggest 3 products in category Elektronika, budget 500 EUR, for Profesionalci..."

3️⃣ Gemini vrne JSON

{ "suggestions": [ { "name": "Pametna tipkovnica", ...} ] }

4️⃣ Prikaz na UI

3 kartice s produktnimi predlogi, povzetek, cene

🎓 Zaključek

Primerjava tehnologij

Aspect .NET MAUI Blazor WebAssembly
Platforma Windows, macOS, iOS, Android Spletni brskalnik
UI Sintaksa XAML Razor (HTML-like)
Dostop do naprave Popoln (kamere, GPS, senzorji) Omejen (le brskalnik API)
Distribucija App stores, instalacijske datoteke Enostavna (URL link)
Performance Native hitrost WebAssembly (blizu native)
Offline support Vgrajeno Potreben PWA setup

Naučene lekcije

  • Async/Await: Kritično za ne-blokajoče UI operacije pri API klicih
  • ObservableCollection (MAUI): Avtomatska posodobitev UI brez ročnega refresha
  • Computed properties (Blazor): Reaktivne lastnosti za dinamično filtriranje
  • Component reusability: Custom komponente (StatusMessage, ConfirmDialog) zmanjšajo podvajanje kode
  • Validation patterns: Data Annotations + EditForm zagotavljajo robustno validacijo
  • JavaScript Interop: Omogoča uporabo obstoječih JS knjižnic (Chart.js) v Blazor
  • REST API best practices: Centraliziran servis za vse API klice, pravilno obravnavanje status kodov

Možne nadgradnje

🔐 Avtentikacija

JWT tokens, OAuth2

💾 Lokalna baza

SQLite za offline-first pristop

🔔 Push notifikacije

Real-time obvestila (MAUI)

📸 Delo s slikami

Upload, crop, filters

🌍 Lokalizacija

Večjezična podpora

🎨 Teme

Dark/Light mode toggle

📋 Povzetek

Projekt zajema implementacijo treh naprednih .NET funkcionalnosti:

  • MAUI Aplikacija - Cross-platform native aplikacija za upravljanje objav z intuitivnim UI
  • Blazor Aplikacija - Spletna aplikacija s podatkovnimi vizualizacijami in CRUD operacijami
  • Gemini LLM Integracija - AI asistent za dinamično generiranje produktnih predlogov

Vse zahteve iz navodil (CRUD operacije, validacija, sortiranje, iskanje, komponente, grafi, responsive design, LLM integracija) so bile uspešno implementirane z best practice pristopom.

3

Naloge

100%

Zahtev izpolnjenih

10+

Komponent

1

AI Model