Портирование WPF приложений на netcore 3.0

    Ожидаемый релиз netcore 3.0 позволяет запускать wpf на netcore. Процедура перевода для одного несложного проекта занимает один-два дня. Каждый последующий — много быстрее.







    Подготовка и конвертация проектов



    Первый этап подготовки — установить и запустить Portability Analyzer. На выходе получим Excel табличку, в которой увидим на сколько наш код соответствует новым требованиям.





    Процедуру конвертации старых проектов провернули в несколько этапов.


    1. Microsoft рекомендует поднять для старых проектов версию фреймворка до .Net Framework 4.7.3.
    2. Сконвертировать структуру старых проектов в новый формат. Заменить packages.config на PackageReference.
    3. В-третьих, скорректировать структуру файла csproj в формат netcore.


    Хочу поблагодарить Янгирова Эмиля с его докладом по миграции на netcore, который очень пригодился. Ссылка на его доклад.



    Получилось, что первый этап мы решили пропустить. Второй этап подразумевал необходимость конвертации больше 100 проектов. Как осуществляется этот процесс можно подробно прочитать здесь.


    Поняли, что без автоматизации никак не обойтись. Воспользовались уже готовым решением: CsprojToVs2017. Пусть название проекта вас не смущает: утилита конвертит и для Visual Studio 2019.



    Что произойдёт?


    Уменьшится размер файлов csproj. За счет чего? Из csproj уйдут все подключенные файлы с исходным кодом, уберутся лишние строки и т.п.



    -    <Compile Include="Models\ViewModels\HistoryViewModel.cs" />
    -    <Compile Include="Properties\Settings.Designer.cs">
    -      <AutoGen>True</AutoGen>
    -      <DependentUpon>Settings.settings</DependentUpon>
    -      <DesignTimeSharedInput>True</DesignTimeSharedInput>
    -    </Compile>
    


    Сократятся записи подключенных библиотек и подпроектов.



    -    <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
    -      <HintPath>..\packages\NLog.4.3.3\lib\net45\NLog.dll</HintPath>
    -      <Private>False</Private>
    -    </Reference>
    -    <ProjectReference Include="..\WpfCommon\WpfCommon.csproj">
    -      <Project>{7ce118f6-2978-42a7-9e6a-ee5cd3057e29}</Project>
    -      <Name>WpfCommon</Name>
    -    </ProjectReference>
    +    <PackageReference Include="NLog" Version="4.6.7" />
    +    <ProjectReference Include="..\WpfCommon\WpfCommon.csproj" />
    


    Общие настройки для нескольких проектов можно унести в Directory.BuildProps. Это такой специальный файл, в который заглядывает MsBuild.
    По аналогии с .gitignore и .editorconfig у нас есть глобальный файл с общими настройками.
    Частные настройки PropertyGroup для подкаталогов/проектов добавляем в конкретные csproj файлы. Подробно можно почитать здесь.



    Зависимости



    Старые зависимости будут для netframework. Придется найти альтернативу или похожие пакеты для nuget. Для многих проектов уже есть Nuget-пакет, которые поддерживают netcore или netstandard.



    К примеру, в проекте использовалась старая версия DI Unity. При переходе на новую версию пришлось обновить using и поправить код в двух-трёх местах.


    using Microsoft.Practices.Unity -> using Unity;
    


    А возможно будет достаточно апнуть все версии пакетов. И на всякий случай перезапустить студию.



    Изменить csproj на использование netcore



    В проектах, которые используют WPF контролы, нужно изменить формат на Microsoft.NET.Sdk.WindowsDesktop:



    -<?xml version="1.0" encoding="utf-8"?>
    -<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    -    <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
    -    <PropertyGroup/>
    

    +<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
    +    <PropertyGroup>
    +        <TargetFramework>netcoreapp3.0</TargetFramework>
    +        <AssemblyTitle>MyEnterpriseLibrary</AssemblyTitle>
    +        <Product>MyEnterpriseLibrary</Product>
    +        <OutputPath>..\bin\$(Configuration)\</OutputPath>
    +        <UseWPF>true</UseWPF>
    +        <!--Если уже есть файл assemblyinfo и он вас устраивает, то следует добавить -->
    +        <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
    </Project>
    


    Для ClassLibrary достаточно оставить тип Microsoft.NET.Sdk:



    <Project Sdk="Microsoft.NET.Sdk">
        <PropertyGroup>
            <TargetFramework>netcoreapp3.0</TargetFramework>
            <AssemblyTitle>MyEnterpriseLibrary</AssemblyTitle>
            <Product>MyEnterpriseLibrary</Product>
            <OutputPath>..\bin\$(Configuration)\</OutputPath>
        </PropertyGroup>
    
        <!-- ... -->
    </Project>
    


    Возможно, в некоторых проектах, которые используются контролы Windows Forms придётся ещё и воткнуть обращение к UseWindowsForms:


    <UseWindowsForms>true</UseWindowsForms>
    


    В csproj изменился подход к потоку компиляции ресурсов. Раньше формат позволял подключить файл и к ресурсам, и к Content,
    и хоть куда.


    Теперь, если файл попал в какую-то коллекцию, то его нужно из неё вытащить, а уже потом включить в нужную группу.
    Вот код, который вытаскивает file.json из коллекции None и подключает его к коллекции Resource.



    <ItemGroup>
        <None Exclude="file.json" />
        <Resource Include="file.json" />
    </ItemGroup>
    


    Соответственно, все файлы, которые не являются исходниками, надо вытащить из коллекции None и подключить к ресурсам. Например, так:



    <ItemGroup Condition="'$(UseWPF)' == 'true' And $(UseWindowsForms) != 'true'">
        <None Exclude="**\*.xml;**\*.xsl;**\*.xslt;**\*.txt;**\*.bmp;**\*.ico;**\*.cur;**\*.gif;**\*.jpeg;**\*.jpe;**\*.jpg;**\*.png;**\*.dib;**\*.tiff;**\*.tif;**\*.inf;**\*.compositefont;**\*.otf;**\*.ttf;**\*.ttc;**\*.tte" />
        <Resource Include="**\*.xml;**\*.xsl;**\*.xslt;**\*.txt;**\*.bmp;**\*.ico;**\*.cur;**\*.gif;**\*.jpeg;**\*.jpe;**\*.jpg;**\*.png;**\*.dib;**\*.tiff;**\*.tif;**\*.inf;**\*.compositefont;**\*.otf;**\*.ttf;**\*.ttc;**\*.tte" />
    </ItemGroup>
    


    Некоторые строки придётся удалить, так как сбивают версию фреймворка на .net framework 4.0.



        Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets"
    


    Кое-где после конвертации останутся странные записи, которые не дают проекту компилироваться. Вот примеры таких конструкций:



    -    <ItemGroup>
    -        <EmbeddedResource Include="**\*.resx" />
    -    </ItemGroup>
    -    <Compile Remove="something.cs">
    


    Клиенты WCF



    Если использовался WCF, то придётся перегенерировать привязки. Как это сделать правильно можно прочитать тут: docs.microsoft.com/en-us/dotnet/desktop-wpf/migration/convert-project-from-net-framework#updating-wcf-client-usage



    Что не взлетит?



    Stylecop и анализ кода.



    В части наших проектов использовались статические анализаторы кода. При переходе на современные редакции MsBuild сборщик явно предлагает использовать вместо старых статических анализаторов кода новые Roslyn-анализаторы.



    Пришлось перевести старые правила на использование Nuget-пакетов Stylecop.Analyzers и FxCop.Analyzers следуя этому руководству Microsoft..
    Если у вас несколько проектов в разных папках (монорепозиторий), то гораздо удобнее вынести подключение анализаторов в Build.props и настраивать едиными ruleset.



    Вот что получилось:



    - <RunCodeAnalysis>true</RunCodeAnalysis>
    + <PackageReference Include="FxCop.Analyzers" Version="2.9.4" />
    


    Файлы — сироты



    Старый формат csproj подразумевал явное подключение .cs файлов. При этом иногда при переименованиях или рефакторингах старые файлы удалялись из csproj, но не удалялись явно с файловой системы. В новом формате csproj автоматически подхватятся все файлы, которые находятся в папке с проектом, как раз те самые, которые не были удалены ранее. Скорее всего в них будут ошибки, обращения к уже несуществующим классам, методам, ресурсам. Выльется в банальные ошибки при сборке.



    Ресурсы



    В одном из проектов использовались SplashScreen, один из которых случайно выбирался при запуске. Экземпляру SplashScreen при инициализации скармливался путь к ресурсу. Почему-то на netcore 3 победить не удалось: ругается на отсутствие ресурса.



    Код, который вроде работает



    Код, который работал в .Net Framework, с большой вероятностью заработает и в netcore. Но могут быть участки кода, на которые компилятор закрыл глаза. В этом случае, если код доберется до инструкций, которые не реализованы в netcore, мы словим PlatformException.


    Для того, чтобы поискать такие места, есть специальный анализатор: github.com/dotnet/platform-compat .



    Зачем всё это, если проект работает?



    Преимуществ не так много, но тем не менее, они есть.



    • Ваш код получит все оптимизации, добавленные в netcore.
    • Увеличится скорость запуска приложения.
    • Нацеливание на будущие версии C#.
    • Уменьшится время сборки проектов благодаря новым версиям csproj.
    • Упаковка в единственный exe.


    Microsoft не подталкивает к переводу приложений на новые рельсы. Тем не менее, если ваше приложение является плагином другого бОльшего, то есть смысл нацелиться на будущие релизы, которые могут быть и на netcore.



    Полезные ссылки



    Directum
    Цифровизация процессов и документов

    Комментарии 18

      +1

      Особых проблем с переносом старого WPF-приложения на новую платформу не было. Но пока не получилось упаковать его в один exe-файл.
      Команда dotnet publish -r win-x64 -c Release /p:PublishSingleFile=true не дала ожидаемого результата. Видимо, я что-то упускаю.

        0
        Пробовали поиграться с этими флагами в запускаемом проекте?

            <RuntimeIdentifier>win-x64</RuntimeIdentifier>
            <PublishSingleFile>true</PublishSingleFile>
            <PublishTrimmed>true</PublishTrimmed>
        


        Мне пока удалось завести компиляцию в единый exe для простых Wpf приложений. Для старых и сложных что-то идет не так, но у меня и ворнинги при сборке выдаются, руки не дошли их починить.
          +1
          Спасибо, работает.
          PublishSingleFile создает один файл как для Windows, так и для Linux (консольных) приложений.
          А PublishTrimmed на сколько понял подразумевает включение Self-contained и выдает ошибку если включено Framework Dependent.

          Единственное но — эти флаги лучше вставлять в Publish-профили, а не в общий файл проекта. Надеюсь, в будущем сделают соответствующие опции.

          Но в целом удобно, хоть и запускается чуть дольше.
            +1
            PublishTrimmed — исключает из публикуемого файла неиспользуемые компоненты (вместо того, чтобы включать всю среду .NET Core). Работает только для автономных приложений, насколько я понимаю.

            Кстати, в студии 16.4 уже добавили опции:
            Заголовок спойлера
            image
              0
              PublishReadyToRun — с ним побыстрей запускается, но распухает приложение.
                0
                PublishReadyToRun — это уже AOT-компиляция…
            +1

            Publish Trimmed же пока не рекомендуют использовать для wpf

        0

        Получается после перевода wpf приложения на .net core всем пользователям нужно будет поднять версию net framework на 4.7? А что делать если это не допустимо заставлять всех пользователей обновлять фреймворк?

          0

          Если у вас.ехе, то вы просто компилируете её под нужную [версию] ОС и делаете её "self-contained". Или я неправильно понял проблематику?

            0
            Проблему поняли правильно. Компилирую WPF приложение, написанное на .NET Core 3.0, под нужную ОС, делаю ее «self-contained». Отдаю пользователю приложение. У пользователю стоит net
            framework 4.5/4.6. WPF приложение заработает?
              +1

              Если вы скомпилировали под ту ОС, которая стоит у клиента, то да.


              P.S. Просто .exe будет относительно большая так как в неё запихаются все нужные библиотеки от фреймворка. То есть если вы отсылаете клиенту несколько разных .exe, то они все будут иметь внутри "редундантные" библиотеки и тогда возможно логичнее уговорить клиента установить у себя нужный фреймворк.

                0
                Только не .NET Framework, a .NET Core Runtime. Всё же — пока это разные платформы.
                  0
                  Только не .NET Framework, a .NET Core Runtime. Всё же — пока это разные платформы.

                  Вы это о том библиотели какого фреймворка запихают в.ехе? Тогда да, естественно это будет .NET Core Runtime.

                    0
                    Я имею виду установку на комп клиенту.
                +1
                Если «self-contained», то не важно какая версия установлена или совсем отсутствует. Все нужные библиотеки идут в комплекте. Собственно, поэтому и называется «self-contained».

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое