Friday, November 3, 2023

WPF, MVVM (CommunityTookit.Mvvm) and Dependency Injection

Projelerim için genel olarak bir template oluşturmak için bu yazıyı yazıyorum. Daha sonrasında da kontrol eder ve burayı temel alırım diye düşünüyorum. Bu yazının amacı teorik olarak ilerlemeyecek daha çok pratik ve sonuç odaklı bir şekilde ele alınacaktır. 

Öncelikle .NET 8 üzerinden bir WPF oluşturuldu. Daha sonrasında MVVM mimarisini projeye uygulayacağız. Proje dizininde üç temel klasörü şimdiden oluşturalım:
  • Models
  • ViewModels
  • Views
Views klasöründe TestView adında basit bir şekilde hazırlamış olduğum bir UserControl .xaml dosya oluşturdum:
<UserControl x:Class="LDG_WPF.Views.TestView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:LDG_WPF.Views"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>

        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"></ColumnDefinition>
                <ColumnDefinition Width="auto"></ColumnDefinition>
            </Grid.ColumnDefinitions>


            <TextBox Grid.Column="0" Width="200" MaxLength="100">

            </TextBox>
            <Button Grid.Column="1" Content="Export" />
        </Grid>

        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"></ColumnDefinition>
                <ColumnDefinition Width="2*"></ColumnDefinition>
            </Grid.ColumnDefinitions>


            <TextBlock Grid.Column="0"></TextBlock>
        </Grid>
    </Grid>
</UserControl>
 
Şimdi sıra logic katmanı görevi görecek olan ViewModel oluşturmada. Normalde ViewModel'ler için bir base class oluşturup onun üzerinden property değişikliklerini handle etmemizi sağlayan bir yapı kullanılırdı. Ancak bu bir nokta zahmet olmaya başladığı için ve bu gibi işleri bizim yerimize yapan paketler çıktığı için artık bu tarz bir yönelime girmeye gerek yok. Microsoft tarafından geliştirilen CommunityToolkit.Mvvm bizim işimizi fazlasıyla görecektir. Nuget Package Manager üzerinden indirebilirsiniz.  Bu sayede ICommand veya INotifyPropertyChanged gibi interface'leri custom olarak  kendimiz uygulamak zorunda kalmadık. 
public partial class TestViewModel: ObservableObject
{
    [ObservableProperty]
    string? displayText;
    [ObservableProperty]
    string? inputText;

    [RelayCommand]
    public void SetText()
    {
        DisplayText = InputText;
    }
}

Şimdi sıra bu ViewModel'i yazdığımız TestView'e bağlamaya geldi. İlk olarak .xaml dosyasından ViewModel'leri bulabilmek için namespace tanımlamak zorundayız, daha sonrasında bu ViewModel'i View datacontext'ine bağlamak için UserControl.DataContext kullanmaktayız:
<UserControl x:Class="LDG_WPF.Views.TestView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:viewmodels="clr-namespace:LDG_WPF.ViewModels"
             xmlns:local="clr-namespace:LDG_WPF.Views"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">

    <UserControl.DataContext>
        <viewmodels:TestViewModel x:Name="test"/>
    </UserControl.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>

        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"></ColumnDefinition>
                <ColumnDefinition Width="auto"></ColumnDefinition>
            </Grid.ColumnDefinitions>

            <TextBox Grid.Column="0" Width="200" MaxLength="100" Text="{Binding InputText}">

            </TextBox>
            <Button Grid.Column="1" Content="Export" Command="{Binding SetTextCommand}" />
        </Grid>

        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"></ColumnDefinition>
                <ColumnDefinition Width="2*"></ColumnDefinition>
            </Grid.ColumnDefinitions>

            <TextBlock Grid.Column="0" Text="{Binding DisplayText}"></TextBlock>
        </Grid>
    </Grid>
</UserControl>

Not: Ayrıca ViewModel bulunamadı gibi bir hata alırsanız muhtemelen projeyi rebuild yaparak sorunu ortadan kaldırabilirsiniz.

Test etmek için TestView'i MainView üzerinden çağırmalıyız:

<Window x:Class="LDG_WPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:LDG_WPF"
        xmlns:views="clr-namespace:LDG_WPF.Views"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <views:TestView/>
    </Grid>
</Window>
Textbox'a bir şeyler yazarsak ve export dersek, yazdığımız yazı TextBlock üzerinde görünecektir. Hey hey hey. CommunityToolkit.Mvvm işi bayağı kolaylaştırdı:

Artık sıra geldi bir Dependecy Injection olayını WPF projesi üzerinde uygulamaya. Dependecy Injection'u IoC container teknoloji olan Microsoft.Extensions.DependencyInjection, Microsoft.Extensions.Configuration.Abstractions ve Microsoft.Extensions.Options.ConfigurationExtensions  nuget paketi kullanarak uygulayacağız. App.xaml.cs dosyasında aşağıdaki kodları yazalım:
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Configuration;
using System.Data;
using System.Windows;

namespace LDG_WPF
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        public IServiceProvider ServiceProvider { get; private set; }

        public  IConfiguration Configuration { get; private set; }

        protected override void OnStartup(StartupEventArgs e)
        {
            var builder = new ConfigurationBuilder();

            Configuration = builder.Build();

            var serviceCollection = new ServiceCollection();
            ConfigureServices(serviceCollection);

            ServiceProvider = serviceCollection.BuildServiceProvider();
            
            var mainWindow = ServiceProvider.GetRequiredService<MainWindow>();
            mainWindow.Show();
        }

        private void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient(typeof(MainWindow));
        }
    }
}
Ayrıca App.xaml dosyasından StartupUri="MainWindow.xaml" opsiyonunu kaldırdım çünkü aynı pencereden tane açılmasına sebep oluyordu:
<Application x:Class="LDG_WPF.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:LDG_WPF"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
         
    </Application.Resources>
</Application>