Расположение объекта xaml в отдельный файл проекта

Доброго времени суток всем)

Не знаю, как точно сформулировать свой вопрос, поэтому попытаюсь описать его очень детально.

У меня есть в проекта xaml-код основного окна, и, соответственно, там же расположен визуальный редактор. У меня есть задача нарисовать достаточное большое кол-во разных фигур (и в будущем их число будет увеличиваться). Все эти фигуры будут вызываться в ходе работы программы в нном кол-ве. Я хотел бы(если что-то подобное в VS вообще есть), сохранять каждую фигуры в отдельный xaml файл, и позже их использовать програмно.

Я мог бы нарисовать все эти фигуры в основном коде xaml, и поставить Visible=Collapsed, а позже копировать эти фигуры в коде, и изменять их размер и изображение. Но этот способ максимально недубен. К тому же, не хотелось бы все фигуры хранить в основном коде xaml, чтобы не нагружать этот код.

И сразу вторую часть вопроса хотел бы задать. Если способ хранить фигуры в отдельных xaml файлах (которые будут расположены в папке в моей текущем проекте) есть (в чем я почти уверен, просто не знаю, как это сделать), то есть ли возможность добавлять отдельные xaml файлы в проект, но вместе с их визуальным редактором?

(Как я себе это представляю, чисто теоретически):

Я кликаю ЛКМ на папку в проекте, нажимаю добавить. Далее я выбираю что-то, что добавит мне xaml код с визуальным редактором. (как xaml код с редактором окна, только без связи с програмной частю, пока-что я сам не задействую xaml-код програмно).

Вооот. Надеюсь я смог адекватно объяснить свою мысль. Спасибо большой тем, кто дочитал сюда:)

Буду очень благодарен, если кто сможет как-то помочь.


Ответы (1 шт):

Автор решения: EvgeniyZ

Я вам покажу самый простейший вариант того, как размещать всякие фигуры правильно в WPF проекте, без мудрения с сохранением XAML и прочего.

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

Ок, поняв суть проблемы, давайте реализовывать задумку:

  1. Создадим простой класс, в котором будет храниться та информация, которая нужна нам для вывода объекта на экран, допустим это будет цвет и положение (X/Y), фигура пока пусть будет простой круг:

     public class Circle
     {
         public double X { get; set; }
         public double Y { get; set; }
         public Brush Fill { get; set; }
     }
    

    Как видите, примитивные данные, ничего лишнего.

  2. Теперь сделаем коллекцию в виде свойства, в которую сразу добавим несколько наших кругов, писать это будет прям в классе окна, но вам советую сделать отдельный класс для данных:

     public List<Circle> Shapes { get; set; } = new()
     {
         new(){ Fill = Brushes.Black, X = 0, Y = 0 },
         new(){ Fill = Brushes.Red, X = 30, Y = 30 },
         new(){ Fill = Brushes.Green, X = 60, Y = 60 },
         new(){ Fill = Brushes.Purple, X = 90, Y = 90 },
     };
    

    Обратите внимание, это свойство, ведь привязка доступна только к публичным свойствам. Также заметьте, тут простой List<>, если вы будете добавлять данные в момент работы приложения, то лучше заменить на ObservableCollection<>, иначе интерфейс не будет оповещаться об изменениях.

  3. Теперь надо окну задать источник данных, из которого он будет брать свойства для привязки, пишем прям в конструкторе, после инициализации DataContext = this;, но опять, это чисто примитивный пример, так лучше не делать на практике, класс окна у вас должен быть чистым, а DataContext задан до момента инициализации окна (почему и как).

  4. Супер, у нас есть теперь данные, осталось вывести их на экран. Открываем XAML окна и пишем нечто такое:

     <ItemsControl ItemsSource="{Binding Shapes}">
         <ItemsControl.ItemTemplate>
             <DataTemplate>
                 <Ellipse
                     Width="30"
                     Height="30"
                     Fill="{Binding Fill}" />
             </DataTemplate>
         </ItemsControl.ItemTemplate>
         <ItemsControl.ItemsPanel>
             <ItemsPanelTemplate>
                 <Canvas />
             </ItemsPanelTemplate>
         </ItemsControl.ItemsPanel>
         <ItemsControl.ItemContainerStyle>
             <Style TargetType="ContentPresenter">
                 <Setter Property="Canvas.Left" Value="{Binding X}" />
                 <Setter Property="Canvas.Top" Value="{Binding Y}" />
             </Style>
         </ItemsControl.ItemContainerStyle>
     </ItemsControl>
    

    Здесь мы берем ItemsControl, который выводит все объекты коллекции на экран и привязываем его к нашей созданной коллекции; далее задаем через ItemTemplate нужный вид с привязкой к нужным свойствам; далее через ItemsPanel задаем панель, на который будут размещены объекты (Canvas); ну и последнее, через ItemContainerStyle задаем им позицию на Canvas.

Запускаем, смотрим на результат:

Result

Как видите, мы без проблем отделили данные от визуальной части, нам не нужно теперь лезть в XAML для того, чтобы добавить, к примеру новый круг, все это просто делается добавлением нового объекта в коллекцию.

Как нам улучшить приложение, если должно быть несколько фигур?
А по сути также, только нам надо сделать еще один класс, вынести общую логику в абстракцию/интерфейс, ну и дополнить XAML.

  1. Вынесем в интерфейс всю общую информацию фигуры. Допустим, мы хотим сделать квадрат, что у него общего с кругом? Цвет, позиция, по сути, в моем примере он дублирует ранее созданный нами класс Circle, давайте тогда вынесем это в interface

     public interface IShape
     {
         public double X { get; }
         public double Y { get; }
         public Brush Fill { get; }
     }
    
  2. Унаследуем круг от созданного интерфейса:

     public class Circle : IShape
     {
         public double X { get; set; }
         public double Y { get; set; }
         public Brush Fill { get; set; }
     }
    
  3. Создадим новый класс, допустим квадрат, который также будет унаследован от IShape:

     public class Square : IShape
     {
         public double X { get; set; }
         public double Y { get; set; }
         public Brush Fill { get; set; }
     }
    
  4. Изменим коллекцию объектов, пусть там будет несколько квадратов и кругов, а сама коллекция будет уже List<IShape>:

     public List<IShape> Shapes { get; set; } = new()
     {
         new Circle(){ Fill = Brushes.Black, X = 0, Y = 0 },
         new Circle(){ Fill = Brushes.Red, X = 30, Y = 30 },
         new Square(){ Fill = Brushes.Green, X = 60, Y = 60 },
         new Square(){ Fill = Brushes.Purple, X = 90, Y = 90 },
     };
    
  5. Теперь XAML, нам надо вынести вид объектов из ItemTemplate в ресурсы:

    1. Удаляем <ItemsControl.ItemTemplate>, запомнив его DataTemplate.

    2. Пишем на его месте <ItemsControl.Resources> и помещаем внутрь DataTemplate. Вы можете поместить их в ресурсы окна, задав окну (Window.Resources), или можете поместить в App.xaml файл, что будет ресурсом приложения, либо вообще вынести в отдельный проект, это уже решайте сами. Должно быть примерно следующее:

      <ItemsControl.Resources>
          <DataTemplate>
              <Ellipse
                  Width="30"
                  Height="30"
                  Fill="{Binding Fill}" />
          </DataTemplate>
      </ItemsControl.Resources>
      
  6. Вынеся нужный вид в ресурсы, мы теперь можем их задавать под тип входного объекта, делается это при помощи указания DataType у DataTemplate. Давайте пропишем квадрату и кругу все нужное:

     <ItemsControl.Resources>
         <DataTemplate DataType="{x:Type local:Circle}">
             <Ellipse
                 Width="30"
                 Height="30"
                 Fill="{Binding Fill}" />
         </DataTemplate>
         <DataTemplate DataType="{x:Type local:Square}">
             <Rectangle 
                 Width="30"
                 Height="30"
                 Fill="{Binding Fill}" />
         </DataTemplate>
     </ItemsControl.Resources>
    

Запускаем, смотрим результат:

Result 2

Как видите, теперь у нас 2 фигуры, данные которых в одном месте, а вид в другом. Имея все это, вы теперь без проблем сможете выбрать нужный вам тип хранения, будь то база данных, или простая сериализация в конкретный тип, это уже решайте сами, главное, не работайте с XAML, как с источником данных. Если вам нужно видеть эти изменения и в дизайнере студии, то для этого задайте окну d:DataContext="{d:DesignInstance {x:Type local:MainWindow}, IsDesignTimeCreatable=True}", где IsDesignTimeCreatable - это подгрузка реальных данных, но учтите, этот способ может сильно нагрузить студию, особенно когда данных много. Чтобы не грузить все данные, можете сделать специальные данные для дизайнера.

→ Ссылка