c# - 源代码更改时 WPF 自动构建和重新加载
问题描述
我看到了一些关于它的答案,建议使用 Live Unit Testing 功能。所以我没有 Microsoft Enterprise,因此无法使用它的 Live Unit Testing 功能。试图创建一个简单的应用程序以编译并重新加载到 WPF 容器窗口中。
骨架代码如下所示:
public void RecompileAndReloadPrj ()
{
Grid.Content = null;
ReleaseAsm()
RunMsBuild(targetProject);
Grid.Content = LoadComponetFromAsm(targetASM);
}
不幸的是,让它工作起来有点复杂……有没有人有现成的代码可以发布、提示等,或者提供链接?
解决方案
是的,在几年前,我如您所愿为 WPF 编写了一些代码。但它是非常基本的,并且有很多问题。但我建议你,在调试时检查 Edit XAML 的特性。即使没有断点,它也能很好地工作。只需在调试模式下运行项目并编辑 XAML 文件,即使您的代码通过后面的代码加载它。甚至不需要保存。
我在这里列出了代码和一些关于它的评论:
主窗口.xaml
<Window x:Class="RebuildAndReloadWPF.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:RebuildAndReloadWPF"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" Activated="Window_Activated" >
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition />
</Grid.RowDefinitions>
<Border CornerRadius="6" BorderBrush="Gray" Background="LightGray" BorderThickness="2" >
<StackPanel x:Name="___No_Name_" Background="LightYellow" Orientation="Horizontal">
<Button Click="Button_Click" >Reload</Button>
<CheckBox x:Name="chkReloadOnFocus" VerticalContentAlignment="Center" VerticalAlignment="Center" Content="Reload on focus" Margin="10,0,0,0"/>
<TextBlock x:Name="txtIndicator" VerticalAlignment="Center" Margin="10,0,0,0"/>
<Border x:Name="elmJustNowIndicator" BorderThickness="1" BorderBrush="Black" Background="Orange" CornerRadius="5" Height="21" Width="76" Visibility="Hidden" Margin="10,0,0,0">
<TextBlock Text="Just now" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</StackPanel>
</Border>
<ScrollViewer x:Name="PlaceHolder" Grid.Row="1"/>
</Grid>
</Window>
主窗口.xaml.cs
using Microsoft.Build.BuildEngine;
using System;
using System.IO;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
namespace RebuildAndReloadWPF
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
const string projectPath = @"C:\...\Some-Project.csproj";
const string libFileName = "Some-Lib.dll";
const string ClassName = "NameSpace.UserControlName";
private string projectFileName;
private string projectDirectoryPath;
private string projectBinPath;
private string logFilePath;
private string appDirectoryPath;
private DispatcherTimer indicatorTimer = new DispatcherTimer();
public MainWindow ()
{
projectFileName = Path.GetFileName(projectPath);
projectDirectoryPath = Path.GetDirectoryName(projectPath);
projectBinPath = projectDirectoryPath + @"\bin\Debug";
logFilePath = projectDirectoryPath + @"\bin\Debug\build.log";
appDirectoryPath = AppDomain.CurrentDomain.BaseDirectory;
indicatorTimer = new DispatcherTimer();
indicatorTimer.Interval = TimeSpan.FromMilliseconds(4000);
indicatorTimer.Tick += ( sender, e ) =>
{
elmJustNowIndicator.Visibility = Visibility.Hidden;
indicatorTimer.IsEnabled = false;
};
InitializeComponent();
}
public void ReloadContainer ()
{
PlaceHolder.Content = null;
bool result = RunMsBuild();
if (!result)
{
txtIndicator.Text = "Compile error, see in log file. Compile fail at: " + DateTime.Now.ToLongTimeString();
txtIndicator.Foreground = Brushes.Red;
return;
}
else
{
txtIndicator.Text = "Last build at: " + DateTime.Now.ToLongTimeString();
txtIndicator.Foreground = Brushes.Green;
}
try
{
File.Copy(projectBinPath + @"\" + libFileName, appDirectoryPath + libFileName, true);
}
catch (Exception ex)
{
MessageBox.Show("Can't copy compiled lib file: " + ex.Message);
throw;
}
elmJustNowIndicator.Visibility = Visibility.Visible;
indicatorTimer.IsEnabled = false;
indicatorTimer.IsEnabled = true;
try
{
PlaceHolder.Content = AsmLoad();
}
catch (Exception ex)
{
MessageBox.Show("Laod assembly error" + ex.Message);
}
GC.Collect();
}
public bool RunMsBuild ()
{
Engine eng = new Engine();
FileLogger logger = new FileLogger();
logger.Parameters = "logfile=" + logFilePath;
eng.RegisterLogger(logger);
bool bb = eng.BuildProjectFile(projectPath);
eng.UnregisterAllLoggers();
return bb;
}
public FrameworkElement AsmLoad ()
{
byte[] assemblyFileBUffer = File.ReadAllBytes(appDirectoryPath + @"\" + libFileName);
Assembly asm = AppDomain.CurrentDomain.Load(assemblyFileBUffer);
ContentControl container = (ContentControl)asm.CreateInstance(ClassName);
return (FrameworkElement)container.Content;
}
private void Window_Activated ( object sender, EventArgs e )
{
if (chkReloadOnFocus.IsChecked == true)
ReloadContainer();
}
private void Button_Click ( object sender, RoutedEventArgs e )
{
ReloadContainer();
}
}
}
注释:
上面的代码仅适用于 UserControl 类。您可以扩展代码以支持 Windows 和面板。
上面的代码仅在我们的应用程序窗口激活(焦点)时进行重建。您可以将其展开以响应文件保存。通过与 FileSystemWatcher 一起使用。注意,每个文件的观察者运行事件。因此,您需要在所有事件爆发结束后等待(通过计时器抢劫),并且还建议将 Visual Studio 配置为始终保存为 Ctr+S 组合键。
Microsoft 将 Microsoft.Build.BuildEngine 替换为最新的程序集,并建议与新的 Microsoft.Build 一起使用。我意识到最新的有问题找到最新的工具(MSBuild.exe),如版本 15.0。您可能会收到需要解决方法的错误:Microsoft.Build.Exceptions.InvalidProjectFileException:'工具版本“15.0”无法识别。可用的工具版本为“12.0”、“14.0”、“2.0”、“3.5”、“4.0”。
动态加载程序集后,Dotnet 无法释放程序集。解决不值得努力。我检查了上面的代码并循环运行它,消耗的 RAM 比我想象的要好。而且,我意识到 Core 3.0 有解决方案。所以如果你喜欢这段代码,我建议你尝试将它迁移到 core 3.0
表现。我没有在实际项目中尝试过。但如果你有备用内存和强大的 CPU,我相信这对于小型项目来说效果很好,并且延迟不会超过半秒。我将它与代码更改后的开始调试进行比较。Visual Studio 进入调试模式和退出有很长的延迟。所以愿你得到显着的改善。
顺便说一句,如果您复制调试信息文件(.pdb)以及 lib 文件,VS 将在目标项目发生运行时错误时打开源文件。但是这个文件得到了锁定。然后下一次重新加载失败(这很奇怪,但根据我的检查,如果目标项目是 VB,它就不会发生)。
如果你真的想用这种方法开发。您需要将目标应用程序构建为小项目的集合。您可以在目标项目中嵌入以下代码,并在开发模式下打开容器窗口。您可以为从文件或外部资源(如数据库)读取的所有数据添加缓存系统。并构建允许将重载直接跳转到界面中的某个点的系统。