forked from amkovkov/GranuSightSoftware2
feat: MainView
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<Application
|
||||
x:Class="GSS2.Application"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:GSS2"
|
||||
RequestedThemeVariant="Dark">
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
</Application.Styles>
|
||||
|
||||
</Application>
|
||||
@@ -0,0 +1,152 @@
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
|
||||
using GSS2.ViewModels;
|
||||
using GSS2.Views;
|
||||
using GSS2.UI.Core;
|
||||
using GSS2.UI.Core.Services;
|
||||
using GSS2.UI.Core.ViewModels;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace GSS2;
|
||||
|
||||
public partial class Application : Avalonia.Application
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly bool _fullscreen;
|
||||
private readonly ILogger<Application> _logger;
|
||||
private readonly DependencyInjectionViewLocator _viewLocator;
|
||||
|
||||
private bool _allShutdownSequencesRunning;
|
||||
|
||||
public Application(IServiceProvider services, bool fullscreen = false)
|
||||
{
|
||||
_services = services;
|
||||
_fullscreen = fullscreen;
|
||||
|
||||
// Создаём AvaloniaLogSink, который перенаправляет логи из Avalonia в хост
|
||||
var loggerFactory = _services.GetRequiredService<ILoggerFactory>();
|
||||
Avalonia.Logging.Logger.Sink = new AvaloniaLogSink(loggerFactory);
|
||||
|
||||
// Создаём DependencyInjectionViewLocator
|
||||
_logger = _services.GetRequiredService<ILogger<Application>>();
|
||||
_viewLocator = _services.GetRequiredService<DependencyInjectionViewLocator>();
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
|
||||
// Добавляем наш DependencyInjectionViewLocator в DataTemplates приложения
|
||||
DataTemplates.Add(_viewLocator);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
// Получаем lifetime хоста
|
||||
var hostLifetime = _services.GetRequiredService<IHostApplicationLifetime>();
|
||||
|
||||
// Регистрируем обработчик начатия Ctrl+C
|
||||
Console.CancelKeyPress += (_, _) =>
|
||||
{
|
||||
_logger.LogInformation("Закрытие приложения вызвано из консоли");
|
||||
hostLifetime.StopApplication();
|
||||
};
|
||||
|
||||
// Создаём обработчик события закрытия хоста
|
||||
hostLifetime.ApplicationStopped.Register(() =>
|
||||
{
|
||||
// Если всё что нужно для закрытия приложения уже вызвано, то ничего не делаем
|
||||
if (_allShutdownSequencesRunning)
|
||||
return;
|
||||
|
||||
// Иначе это событие вызвано не из интерфейса и графический интерфейс нужно закрыть
|
||||
Dispatcher.InvokeShutdown();
|
||||
_allShutdownSequencesRunning = true;
|
||||
});
|
||||
|
||||
// Создаём обработчик события закрытия графического интерфейса
|
||||
Dispatcher.ShutdownStarted += (_, _) =>
|
||||
{
|
||||
// Если всё что нужно для закрытия приложения уже вызвано, то ничего не делаем
|
||||
if (_allShutdownSequencesRunning)
|
||||
return;
|
||||
|
||||
// Иначе это событие вызвано из интерфейса и хост нужно закрыть
|
||||
_logger.LogInformation("Закрытие приложения вызвано из интерфейса");
|
||||
hostLifetime.StopApplication();
|
||||
_allShutdownSequencesRunning = true;
|
||||
};
|
||||
|
||||
// ApplicationLifetime == null, то невозможно определить в какой среде мы запускаемся,
|
||||
// но такого быть не должно
|
||||
if (ApplicationLifetime is null)
|
||||
{
|
||||
_logger.LogCritical("Приложение не может быть полностью инициализировано, ApplicationLifetime == null");
|
||||
|
||||
// Я не уверен, будет ли правильно в данном классе вместо выбрасывания исключений,
|
||||
// использовать Environment.Exit,
|
||||
// но если использовать исключения, то их может поймать какой-нибудь обработчик,
|
||||
// а Environment.Exit гарантированно завершит текущий процесс
|
||||
hostLifetime.StopApplication();
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
// Создаём сервис навигации между элементами управления
|
||||
var navigationService = _services.GetRequiredService<NavigationService>();
|
||||
|
||||
// Создаём начальный элемент управления и переходим к нему
|
||||
var mainView = new MainView()
|
||||
{
|
||||
DataContext = _services.GetRequiredService<MainViewModel>()
|
||||
};
|
||||
|
||||
// Создаём главный элемент управления
|
||||
// на основании типа ApplicationLifetime, который зависит от того как мы запустили приложение
|
||||
// StartWithClassicDesktopLifetime IClassicDesktopStyleApplicationLifetime
|
||||
// StartLinuxDrm ISingleViewApplicationLifetime
|
||||
switch (ApplicationLifetime)
|
||||
{
|
||||
case IClassicDesktopStyleApplicationLifetime desktop:
|
||||
_logger.LogInformation("Приложение запускается в классическом режиме");
|
||||
_logger.LogInformation("Приложение запускается в {} режиме", _fullscreen ? "полноэкранном" : "оконном");
|
||||
|
||||
// Создаём главное окно
|
||||
var mainWindow = new MainWindow()
|
||||
{
|
||||
DataContext = new MainWindowViewModel()
|
||||
{
|
||||
WindowDecorations = _fullscreen ? Avalonia.Controls.WindowDecorations.None : Avalonia.Controls.WindowDecorations.Full,
|
||||
Topmost = _fullscreen,
|
||||
WindowState = _fullscreen ? Avalonia.Controls.WindowState.FullScreen : Avalonia.Controls.WindowState.Normal
|
||||
},
|
||||
};
|
||||
desktop.MainWindow = mainWindow;
|
||||
|
||||
// Переключаемся на начальный элемент управления
|
||||
navigationService.NavigateTo(mainView);
|
||||
|
||||
break;
|
||||
|
||||
case ISingleViewApplicationLifetime singleView:
|
||||
_logger.LogInformation("Приложение запускается в режиме отображение единой формы");
|
||||
|
||||
// Просто устанавливаем главным элементом управления начальный элемент
|
||||
singleView.MainView = mainView;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
_logger.LogCritical("Приложение не может быть полностью инициализировано, ApplicationLifetime имеет неизвестный тип, наследованный от: {}", string.Join(", ", ApplicationLifetime.GetType().GetInterfaces().Select(i => i.FullName)));
|
||||
hostLifetime.StopApplication();
|
||||
Environment.Exit(1);
|
||||
break;
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
+128
-2
@@ -1,5 +1,17 @@
|
||||
using System.CommandLine;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.OpenGL.Egl;
|
||||
|
||||
using GSS2.Core.Logging;
|
||||
using GSS2.Core;
|
||||
using GSS2.Core.Analysis.AmineContent.Database.Calibration;
|
||||
using GSS2.Core.Analysis.AmineContent.Database.Results;
|
||||
|
||||
namespace GSS2.Commands;
|
||||
|
||||
public static class UI
|
||||
@@ -31,7 +43,6 @@ public static class UI
|
||||
RenderingMode = new("--mode", ["--rendering-mode", "-m"])
|
||||
{
|
||||
Description = "Режим рендеринга",
|
||||
|
||||
DefaultValueFactory = (_) => RenderingModes.X11
|
||||
};
|
||||
HideConsole = new("--hide-console", ["--no-console", "-c"])
|
||||
@@ -53,7 +64,6 @@ public static class UI
|
||||
Command.Add(RenderingMode);
|
||||
Command.Add(HideConsole);
|
||||
Command.SetAction(CommandAction);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -69,6 +79,122 @@ public static class UI
|
||||
|
||||
private static void CommandAction(ParseResult result)
|
||||
{
|
||||
/// Получаем значения аргументов командной строки
|
||||
var fullscreen = result.GetValue(Fullscreen);
|
||||
var renderingMode = result.GetValue(RenderingMode);
|
||||
var hideConsole = result.GetValue(HideConsole);
|
||||
|
||||
// Если выбран режим рендеринга DRM, то приложение будет рисоваться вместе с консолью,
|
||||
// поэтому если не перехватить консоль, то вывод в неё будет рисоваться вместе с интерфейсом
|
||||
if (hideConsole)
|
||||
SilenceConsole();
|
||||
|
||||
// Собираем хост
|
||||
var host = BuildHost();
|
||||
|
||||
// Запускаем хост асинхронно без блокировки потока
|
||||
_ = host.RunAsync();
|
||||
|
||||
// Собираем графическое приложение
|
||||
var ui = BuildApplication(host, fullscreen);
|
||||
switch (renderingMode)
|
||||
{
|
||||
case RenderingModes.X11:
|
||||
// Если мы запускаемся под X11,
|
||||
// то очевидно у нас есть полноценный рабочий стол,
|
||||
// поэтому запускаем графический интерфейс в классическом режиме с главным окном
|
||||
ui.StartWithClassicDesktopLifetime(Program.Args);
|
||||
break;
|
||||
case RenderingModes.DRM:
|
||||
// Если мы запускаемся в DRM (Direct Rendering Mode),
|
||||
// то у нас нет рабочего стола и окон,
|
||||
// потому что отрисовка (вроде как) идёт напрямую в кадровый буфер,
|
||||
// поэтому запускаем графический интерфейс в DRM.
|
||||
// Если работает X11, или любая другая графическая система,
|
||||
// то запустить приложение в DRM не получится,
|
||||
// потому что DRI (Direct Rendering Interface) занят этой графической системой
|
||||
// TODO: Подумать над тем, чтобы брать DRI и scale для DRM рендеринга из переменных среды
|
||||
// TODO: Подумать над тем, чтобы при запуске в DRM убивать приложения, которые занимают DRI
|
||||
ui.StartLinuxDrm(Program.Args, "/dev/dri/card1", 1.0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SilenceConsole()
|
||||
{
|
||||
var start = new ThreadStart(() =>
|
||||
{
|
||||
Console.CursorVisible = false;
|
||||
while (true)
|
||||
Console.ReadKey(true);
|
||||
});
|
||||
var thread = new Thread(start)
|
||||
{
|
||||
IsBackground = true
|
||||
};
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
private static IHost BuildHost()
|
||||
{
|
||||
// Создаём хост
|
||||
var builder = Host.CreateApplicationBuilder();
|
||||
|
||||
// Добавляем конфигурацию из appsettings.json
|
||||
builder.Configuration.AddAppSettingsJson();
|
||||
|
||||
// Конфигурируем логгер
|
||||
builder.Logging.ConfigureLogging();
|
||||
builder.Services.AddSingleton<LibCameraLogSink>();
|
||||
|
||||
// Добавляем сервисы баз данных
|
||||
builder.Services.AddAmineContentCalibrationContext(builder.Configuration);
|
||||
builder.Services.AddAmineContentResultsContext(builder.Configuration);
|
||||
|
||||
// Добавляем сервисы аппаратной части и прочее
|
||||
builder.Services.AddLightsService("Hardware:Lights");
|
||||
builder.Services.AddTemperatureHumidityService("Hardware:TemperatureHumidity");
|
||||
builder.Services.AddIlluminatorService("Hardware:Illuminator");
|
||||
builder.Services.AddCameraService("Hardware:Camera", true);
|
||||
builder.Services.AddComputeResourcesService();
|
||||
builder.Services.AddImageStorageService(new DirectoryInfo("images"));
|
||||
|
||||
// Добавляем элементы интерфейса как сервисы
|
||||
builder.Services.AddViewsAndModels();
|
||||
|
||||
// Собираем хост
|
||||
var host = builder.Build();
|
||||
|
||||
// Поскольку LibCameraLogSink, который перенаправляет логи из libcamera в хост,
|
||||
// не будет создан ни одним из других сервисов, его необходимо создать превентивно
|
||||
host.Services.GetRequiredService<LibCameraLogSink>();
|
||||
|
||||
// Применяем миграции баз данных если их схема данных изменялась
|
||||
using (var scope = host.Services.CreateScope())
|
||||
{
|
||||
var context = scope.ServiceProvider.GetRequiredService<AmineContentCalibrationContext>();
|
||||
context.Database.Migrate();
|
||||
}
|
||||
using (var scope = host.Services.CreateScope())
|
||||
{
|
||||
var context = scope.ServiceProvider.GetRequiredService<AmineContentResultsContext>();
|
||||
context.Database.Migrate();
|
||||
}
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
private static AppBuilder BuildApplication(IHost host, bool fullscreen) =>
|
||||
AppBuilder.Configure(() => new Application(host.Services, fullscreen))
|
||||
.UsePlatformDetect()
|
||||
.UseSkia()
|
||||
.LogToTrace()
|
||||
.With(new X11PlatformOptions
|
||||
{
|
||||
RenderingMode = [X11RenderingMode.Egl]
|
||||
})
|
||||
.With(new EglDisplayOptions
|
||||
{
|
||||
SupportsContextSharing = true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using GSS2.UI.Core.ViewModels;
|
||||
using GSS2.UI.Core.Services;
|
||||
using GSS2.Views;
|
||||
using GSS2.ViewModels;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace GSS2;
|
||||
|
||||
public static class DependencyInjectionHelper
|
||||
{
|
||||
public static IServiceCollection AddViewsAndModels(this IServiceCollection services) =>
|
||||
services
|
||||
.AddSingleton<DependencyInjectionViewLocator>((services) =>
|
||||
new DependencyInjectionViewLocator(
|
||||
services.GetRequiredService<ILogger<DependencyInjectionViewLocator>>(),
|
||||
services
|
||||
)
|
||||
)
|
||||
.AddSingleton<NavigationService>((_) => new NavigationService(Application.Current?.ApplicationLifetime))
|
||||
.AddSingleton<MainWindowViewModel>()
|
||||
.AddSingleton<MainView>()
|
||||
.AddSingleton<MainViewModel>()
|
||||
.AddSingleton<AmineContentView>()
|
||||
.AddSingleton<AmineContentViewModel>();
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
|
||||
using GSS2.UI.Core.ViewModels;
|
||||
|
||||
namespace GSS2;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DependencyInjectionViewLocator"/> - интерфейс между Avalonia и механизмом DI (Dependency Injection)
|
||||
/// из Microsoft.Extensions.Hosting.<br/>
|
||||
/// Создаёт View для переданной ViewModel.<br/>
|
||||
/// Обязательным условием его работы является, то что:
|
||||
/// <list type="bullet">
|
||||
/// <item>Класс <i>View</i> находится в пространстве имён <i>SomeNameSpace.Views</i> и имеет имя <i>SomeClassView</i></item>
|
||||
/// <item>Класс <i>ViewModel</i> находится в пространстве имён <i>SomeNameSpace.ViewModels</i> и имеет имя <i>SomeClassViewModel</i></item>
|
||||
/// <item>Класс <i>ViewModel</i> наследован от <see cref="ViewModelBase"/></item>
|
||||
/// <item>Класс <i>View</i> наследован от <see cref="Control"/></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public class DependencyInjectionViewLocator : IDataTemplate
|
||||
{
|
||||
private readonly ILogger<DependencyInjectionViewLocator> _logger;
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
public DependencyInjectionViewLocator(ILogger<DependencyInjectionViewLocator> logger, IServiceProvider services)
|
||||
{
|
||||
_logger = logger;
|
||||
_services = services;
|
||||
}
|
||||
|
||||
public bool Match(object? data) => data is ViewModelBase;
|
||||
|
||||
public Control? Build(object? data)
|
||||
{
|
||||
// Если data - null или каким-то образом не ViewModelBase, то возвращаем null
|
||||
if (data is not ViewModelBase viewModel)
|
||||
return null;
|
||||
|
||||
// Получаем имя класса ViewModel с полным описанием сборки
|
||||
var viewModelName = viewModel.GetType().AssemblyQualifiedName;
|
||||
if (viewModelName is null)
|
||||
{
|
||||
_logger.LogError("Не удалось получить имя класса ViewModel: {}", viewModel);
|
||||
return new Control();
|
||||
}
|
||||
|
||||
// Заменяем ".ViewModel" на ".View" в имени класса ViewModel и получаем имя класса View и его тип
|
||||
var viewName = viewModelName.Replace("ViewModel", "View");
|
||||
var viewType = Type.GetType(viewName);
|
||||
if (viewType is null)
|
||||
{
|
||||
_logger.LogError("Не удалось найти класс View \"{}\"", viewName);
|
||||
return new Control();
|
||||
}
|
||||
|
||||
// Ищем тип View в сервисах
|
||||
var view = _services.GetService(viewType);
|
||||
if (view is null)
|
||||
{
|
||||
_logger.LogError("Класс View \"{}\" не зарегистрирован", viewName);
|
||||
return new Control();
|
||||
}
|
||||
if (view is not Control control)
|
||||
{
|
||||
_logger.LogError("Класс View \"{}\" не наследован от Control", viewName);
|
||||
return new Control();
|
||||
}
|
||||
|
||||
control.DataContext = viewModel;
|
||||
return control;
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -2,8 +2,17 @@
|
||||
|
||||
public partial class Program
|
||||
{
|
||||
// Список аргументов командной строки
|
||||
// Аргументы командной строки
|
||||
private static List<string> _args = new List<string>();
|
||||
public static string[] Args
|
||||
{
|
||||
get
|
||||
{
|
||||
var copy = new string[_args.Count()];
|
||||
_args.CopyTo(copy);
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Точка входа
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
using GSS2.UI.Core.Services;
|
||||
using GSS2.UI.Core.ViewModels;
|
||||
|
||||
namespace GSS2.ViewModels;
|
||||
|
||||
public partial class MainViewModel : ViewModelBase
|
||||
{
|
||||
private Control? _amineContentAnalysisView = null;
|
||||
|
||||
private readonly NavigationService _navigationService;
|
||||
private readonly DependencyInjectionViewLocator _viewLocator;
|
||||
|
||||
[ObservableProperty] public partial AmineContentViewModel AmineContentAnalysisViewModel { get; set; }
|
||||
|
||||
public MainViewModel(
|
||||
NavigationService navigationService,
|
||||
DependencyInjectionViewLocator viewLocator,
|
||||
AmineContentViewModel amineContentAnalysisViewModel
|
||||
)
|
||||
{
|
||||
_navigationService = navigationService;
|
||||
_viewLocator = viewLocator;
|
||||
|
||||
AmineContentAnalysisViewModel = amineContentAnalysisViewModel;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void NavigateToAmineContentAnalysis()
|
||||
{
|
||||
if (_amineContentAnalysisView is null)
|
||||
_amineContentAnalysisView = _viewLocator.Build(AmineContentAnalysisViewModel);
|
||||
if (_amineContentAnalysisView is not null)
|
||||
_navigationService.NavigateTo(_amineContentAnalysisView);
|
||||
}
|
||||
|
||||
[RelayCommand] public void NavigateToColorimetricAnalysis() { }
|
||||
[RelayCommand] public void NavigateToGranulometricAnalysis() { }
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<UserControl
|
||||
x:Class="GSS2.Views.MainView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:GSS2.ViewModels"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
x:DataType="vm:MainViewModel">
|
||||
|
||||
<UserControl.Resources>
|
||||
<SolidColorBrush x:Key="MainBackground" Color="#001e46" />
|
||||
<SolidColorBrush x:Key="ButtonBackground" Color="#0865b2" />
|
||||
<SolidColorBrush x:Key="ButtonForeground" Color="#ffffff" />
|
||||
<x:Double x:Key="ButtonFontSize">20</x:Double>
|
||||
</UserControl.Resources>
|
||||
|
||||
<UserControl.Styles>
|
||||
|
||||
<Style Selector="Button">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalAlignment" Value="Stretch"/>
|
||||
<Setter Property="Background" Value="{StaticResource ButtonBackground}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource ButtonForeground}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
<Setter Property="TextAlignment" Value="Center"/>
|
||||
<Setter Property="FontSize" Value="{StaticResource ButtonFontSize}"/>
|
||||
</Style>
|
||||
|
||||
</UserControl.Styles>
|
||||
|
||||
<Border Background="{StaticResource MainBackground}" BorderThickness="0">
|
||||
<Grid ColumnSpacing="5" Margin="5">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Button Grid.Column="0" Command="{Binding NavigateToAmineContentAnalysisCommand}">
|
||||
<TextBlock Text="Анализ на кондиционирующие добавки"/>
|
||||
</Button>
|
||||
|
||||
<Button Grid.Column="1" Command="{Binding NavigateToColorimetricAnalysisCommand}" IsEnabled="False">
|
||||
<TextBlock Text="Анализ цветности"/>
|
||||
</Button>
|
||||
|
||||
<Button Grid.Column="2" Command="{Binding NavigateToGranulometricAnalysisCommand}" IsEnabled="False">
|
||||
<TextBlock Text="Анализ гранулометрического состава"/>
|
||||
</Button>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace GSS2.Views;
|
||||
|
||||
public partial class MainView : UserControl
|
||||
{
|
||||
public MainView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user