Journey into pyRevit

Having a good library of drafting view details has proven to be an important component of completing a project quickly and not repeating work. Native Revit has a function "insert views from file" but it has the following drawbacks:

  1. No ability to filter views shown by view type
  2. No ability to zoom in or resize the preview window for larger views
  3. No ability to choose a custom name for the view on import or to choose the view to replace an existing view
  4. No control on the view type of the view after import
  5. Can't export views from the current file to another (ie the same process in reverse)

I sought out to create our own internal tool which would allow us to transfer views to or from our detail library files that solved all these drawbacks using pyRevit. In order for any of the code within this post to be of use to anyone, the below context needs to be understood:

  1. We have a "Master" detail Revit file for each of the major materials--Concrete, Cold Form, Masonry, Steel, Wood. These are stored on a network drive under a common folder, such that the path to a single file resembles the below:
    • network_drive->Structural->Drawing Tools->Revit->Details->Steel->Steel Master File.rvt
  2. Each Master detail Revit file is workshared. This serves a few purposes:
    1. Revit automatically creates and manages backup versions of the files and creates and easy way to add change messages.
    2. A local copy is created on users machines which adds a layer of protection in the event that the server file is lost or compromised.

The general outline of the script operation is as such (note target document is the document that the views are being transferred into):

  1. Verify that the current project Revit version is 2024 (which is what our Master detail files are).
  2. Verify that the library files can be found.
  3. Asks the user to select a library file.
  4. Attempts to read the current users Revit.ini file to determine the directory that local versions of workshared files are stored.
  5. Attempts to open the workshared library file.
  6. Opens the WPF form containing available views to transfer.

Past this point, the user has a WPF form that displays a data grid of all available views with a combo box to filter by drafting view type. The form displays a preview of the currently selected view in the right hand pane that is able to be zoomed, panned and reset. 

The checkbox in the first data grid column is to select the view for transfer. The next column contains the original view name but can be clicked into in order to rename the view upon transfer. The third column is a toggle button that will allow the checked view to replace the matching view if one with the same name is found in the target document. Lastly, the fourth column is a combo box to determine the transferred view type (either "Retain Original" or one of the view types from the target document).

The code pertaining to the WPF form is quite long and is used in both the import and export scripts so I've elected to move it into a library file common to the overall extension. A preview of the import tool is shown below:

NOTE: all of the code below is put together by referencing multiple internet sources and by heavily modifying existing pyRevit functions and forms. I am *not* a programmer so it is likely poorly written and can be improved upon. 

ViewSelectorForm.xaml

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        ShowInTaskbar="False"
        WindowStartupLocation="CenterScreen"
        HorizontalContentAlignment="Center"
	MouseWheel="view_zoom">
    <Window.Resources>
        <Style x:Key="ClearButton" TargetType="Button">
            <Setter Property="Background" Value="White"/>
        </Style>
        <Style TargetType="{x:Type ToggleButton}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ToggleButton}">
                        <Viewbox>
                            <Border x:Name="Border" CornerRadius="10"
                                    Background="#FFFFFFFF"
                                    Width="40" Height="20">
                                <Border.Effect>
                                    <DropShadowEffect ShadowDepth="0.5" Direction="0" Opacity="0.3" />
                                </Border.Effect>
                                <Ellipse x:Name="Ellipse" Fill="#FFFFFFFF" Stretch="Uniform"
                                         Margin="2 1 2 1"
                                         Stroke="Gray" StrokeThickness="0.2"
                                         HorizontalAlignment="Stretch">
                                    <Ellipse.Effect>
                                        <DropShadowEffect BlurRadius="10" ShadowDepth="1" Opacity="0.3" Direction="260" />
                                    </Ellipse.Effect>
                                </Ellipse>
                            </Border>
                        </Viewbox>
                        <ControlTemplate.Triggers>
                            <EventTrigger RoutedEvent="Checked">
                                <BeginStoryboard>
                                    <Storyboard>
                                        <ColorAnimation Storyboard.TargetName="Border"
                                                            Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
                                                            To="#FF4CD661"
                                                            Duration="0:0:0.1" />
                                        <ThicknessAnimation Storyboard.TargetName="Ellipse"
                                                                Storyboard.TargetProperty="Margin"
                                                                To="20 1 2 1"
                                                                Duration="0:0:0.1" />
                                    </Storyboard>
                                </BeginStoryboard>
                            </EventTrigger>
                            <EventTrigger RoutedEvent="Unchecked">
                                <BeginStoryboard>
                                    <Storyboard>
                                        <ColorAnimation Storyboard.TargetName="Border"
                                                            Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
                                                            To="White"
                                                            Duration="0:0:0.1" />
                                        <ThicknessAnimation Storyboard.TargetName="Ellipse"
                                                                Storyboard.TargetProperty="Margin"
                                                                To="2 1 2 1"
                                                                Duration="0:0:0.1" />
                                    </Storyboard>
                                </BeginStoryboard>
                            </EventTrigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <DockPanel Margin="10">
        <StackPanel DockPanel.Dock="Top" Margin="0,0,0,10">
            <DockPanel>
                <StackPanel>
                    <TextBox x:Name="search_tb" Height="25"
                             Padding="5,0,24,0"
                             VerticalContentAlignment="Center"/>
                    <Button x:Name="clrsearch_b" Visibility="Collapsed" Margin="0,-25,2,0" Padding="0,-4,0,0"
                            Width="22px" Height="22px" HorizontalAlignment="Right"
                            BorderThickness="0"
                            Style="{StaticResource ClearButton}"
                            Click="clear_search">
                        <Button.Content>
                            <Canvas Width="25" Height="25" VerticalAlignment="Center" HorizontalAlignment="Center">
                                <Path Canvas.Top="3" Canvas.Left="1" Data="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" Fill="DimGray" />
                            </Canvas>
                        </Button.Content>
                    </Button>
                </StackPanel>
            </DockPanel>
            <DockPanel x:Name="ctx_groups_dock" Margin="0,10,0,0" Visibility="Collapsed">
                <TextBlock x:Name="ctx_groups_title_tb" FontSize="14" Margin="0,3,10,0"
                           DockPanel.Dock="Left">List Group:</TextBlock>
                <ComboBox x:Name="ctx_groups_selector_cb" Height="25"/>
            </DockPanel>
        </StackPanel>
        <StackPanel DockPanel.Dock="Bottom">
            <Button x:Name="select_b" Content="" Height="32" Click="button_select" Margin="0,10,0,0"/>
        </StackPanel>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="3*" />
                <ColumnDefinition Width="2*" />
            </Grid.ColumnDefinitions>
            <DataGrid x:Name="views_dg"
                      Grid.Column="0"
                      SelectionChanged="selected_item_changed"
                      SelectionMode="Extended"
                      HorizontalContentAlignment="Stretch"
                      ItemsSource="{Binding Path=., Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
                <DataGrid.Columns>
                    <DataGridTemplateColumn  x:Name="transfer_col"
                                             Width="Auto">
                        <DataGridColumn.HeaderStyle>
                            <Style TargetType="DataGridColumnHeader">
                                <Setter Property="HorizontalContentAlignment"
                                Value="Center" />
                            </Style>
                        </DataGridColumn.HeaderStyle>
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <CheckBox VerticalAlignment="Center"
                                          HorizontalAlignment="Center"
                                          IsChecked="{Binding checked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                                          Visibility="{Binding checkable}"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTextColumn Header="View Name"
                                        Width="*"
                                        Binding="{Binding view_name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
                        <DataGridColumn.HeaderStyle>
                            <Style TargetType="DataGridColumnHeader">
                                <Setter Property="HorizontalContentAlignment"
                                Value="Center" />
                            </Style>
                        </DataGridColumn.HeaderStyle>
                    </DataGridTextColumn>
                    <DataGridTemplateColumn Header="Replace?"
                                            Width="Auto">
                        <DataGridColumn.HeaderStyle>
                            <Style TargetType="DataGridColumnHeader">
                                <Setter Property="HorizontalContentAlignment"
                                Value="Center" />
                            </Style>
                        </DataGridColumn.HeaderStyle>
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <ToggleButton VerticalAlignment="Center"
                                          HorizontalAlignment="Center"
                                          IsChecked="{Binding replace, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                                          Visibility="{Binding replacable, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn Header="View Type"
                                            Width="Auto">
                        <DataGridColumn.HeaderStyle>
                            <Style TargetType="DataGridColumnHeader">
                                <Setter Property="HorizontalContentAlignment"
                                Value="Center" />
                            </Style>
                        </DataGridColumn.HeaderStyle>
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <ComboBox VerticalAlignment="Center"
                                          IsSynchronizedWithCurrentItem="True"
                                          SelectedValue="{Binding view_type, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                                          ItemsSource="{Binding available_view_types}"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                </DataGrid.Columns>
            </DataGrid>
            <GridSplitter Grid.Column="1"
                VerticalAlignment="Stretch"
                HorizontalAlignment="Stretch"
                ResizeDirection="Columns"
                Visibility="Collapsed"/>
	    <Border x:Name="view_border"
                    Grid.Column="2" 
                    ClipToBounds="True"
                    BorderBrush="Black"
                    BorderThickness="1"
                    HorizontalAlignment="Stretch"
                    VerticalAlignment="Stretch">
                <Image x:Name="view_i"
                       Source="{Binding Path=., Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"                
                       MouseRightButtonDown="view_reset" 
                       MouseLeftButtonDown="view_pan_start" 
                       MouseLeftButtonUp="view_pan_stop" 
                       MouseMove="view_pan_move"/>
            </Border>
        </Grid>
    </DockPanel>
</Window>

DB Tools.py

from pyrevit import revit, DB

from pyrevit import coreutils
from pyrevit import forms
from pyrevit.framework import List, Windows, Media, Controls, ObservableCollection

import os
import shutil
import tempfile
from collections import OrderedDict

class TemporaryDirectory(object):
    def __enter__(self):
        self.name = tempfile.mkdtemp()
        return self.name

    def __exit__(self, exc_type, exc_value, traceback):
        shutil.rmtree(self.name)

class MasterDetailFile:
    def __init__(self, name, path=""):
        self.name = name
        self.path = path

class TransferDraftingViews:
    def __init__(self, doc, targetDoc, transfer_col_header=""):
        self._doc = doc
        self._targetDoc = targetDoc
        self._transfer_col_header = transfer_col_header

        self._draftingViews = [x for x in DB.FilteredElementCollector(self._targetDoc).OfClass(DB.View)\
                               .WhereElementIsNotElementType().ToElements()
                               if x.ViewType == DB.ViewType.DraftingView]

        try:
            with revit.TransactionGroup("DB Tools - Transfer Drafting Views", doc=self._doc, assimilate=True, log_errors=False):
                with TemporaryDirectory() as temp_dir:
                    self._viewSelectorForm = TransferDraftingViews.ViewSelectorForm(self, self._doc, self._targetDoc,
                                                                                    temp_dir, self._draftingViews)
                        
                    self._viewSelectorForm.ShowDialog()
                
        except:
            forms.alert("Unknown error. Aborting...", title="DB Tools", exitscript=True)
            
    class ViewSelectorFormItem(forms.Reactive):
        def __init__(self, TransferDraftingViews, orig_item,
                     checked=False, checkable=True,
                     replace=False, replacable=False):
            super(TransferDraftingViews.ViewSelectorFormItem, self).__init__()
            self.item = orig_item
            self._state = checked
            self._checkable = checkable
            self._available_view_types = []
            self._available_view_types.append("Retain Original")
            self._view_type = self._available_view_types[0]
            self.view_name = self.name
            self._replace = replace
            self._replacable = replacable
            
        def __nonzero__(self):
            return self._state

        def __str__(self):
            return self.name 

        def __contains__(self, value):
            return value in self._name

        def __getattr__(self, param_name):
            return getattr(self.item, param_name)

        @property
        def name(self):
            return self.item.Name

        def unwrap(self):
            return self.item

        @forms.reactive
        def checked(self):
            return self._state

        @checked.setter
        def checked(self, value):
            self._state = value

        @property
        def checkable(self):
            return forms.WPF_VISIBLE if self._checkable else forms.WPF_COLLAPSED

        @checkable.setter
        def checkable(self, value):
            self._checkable = value

        @property
        def available_view_types(self):
            return self._available_view_types

        @property
        def view_type(self):
            return self._view_type

        @view_type.setter
        def view_type(self, value):
            self._view_type = value
        
        @forms.reactive
        def replace(self):
            return self._replace
        
        @replace.setter
        def replace(self, value):
            self._replace = value

        @property
        def replacable(self):
            return forms.WPF_VISIBLE if self._replacable else forms.WPF_COLLAPSED

        @replacable.setter
        def replacable(self, value):
            self._replacable = value  

    class ViewSelectorForm(forms.WPFWindow):
        def __init__(self,  TransferDraftingViews, doc, targetDoc, temp_dir, draftingViews):
            self._wpfPath = os.path.normpath(r"network_drive\Structural\Drawing Tools\Revit\DB Tools PyRevit Extension\DB Tools.extension\WPF\ViewSelectorForm.xaml")
            forms.WPFWindow.__init__(self, self._wpfPath)
            
            self._parent = TransferDraftingViews
            self._doc = doc
            self._targetDoc = targetDoc
            self._temp_dir = temp_dir
            self._draftingViews = draftingViews

            self.Title = "Select Views To Transfer"
            self.transfer_col.Header = self._parent._transfer_col_header
            self.Width = 1200
            self.Height = 600
            
            self.in_check = False
            self.in_uncheck = False

            self._origin = Windows.Point(0, 0)    
            self._start = Windows.Point(0, 0)
            self._reset = self.view_i.RenderTransform.Value
            
            self._result = ""
            self._numAdded = 0
            self._numReplaced = 0
            
            self._viewsToTransfer = []
            self._destViewTypes = []
            self._update_dest_types()
            
            self._targetViewTypes = []
            targetViewTypeCollector = DB.FilteredElementCollector(self._targetDoc).OfClass(DB.ViewFamilyType)

            for targetViewType in targetViewTypeCollector:
                if targetViewType.ViewFamily == DB.ViewFamily.Drafting:
                    if not targetViewType in self._targetViewTypes:
                        self._targetViewTypes.append(targetViewType)

            self._context = {revit.query.get_name(self._targetViewTypes[k]): [self._parent.ViewSelectorFormItem(self._parent,self._draftingViews[x]) for x in range(len(self._draftingViews))
                                                                                  if revit.query.get_name(revit.query.get_type(self._draftingViews[x])) == revit.query.get_name(self._targetViewTypes[k])]
                                                                                  for k in range(len(self._targetViewTypes))}

            for k in self._context:
                for v in self._context[k]:
                    for destViewType in self._destViewTypes:
                        if not revit.query.get_name(destViewType) in v.available_view_types:
                            v.available_view_types.append(revit.query.get_name(destViewType))
                                 
            self._setup()

        def _update_dest_types(self):
            destViewTypeCollector = DB.FilteredElementCollector(self._doc).OfClass(DB.ViewFamilyType)

            for destViewType in destViewTypeCollector:
                if destViewType.ViewFamily == DB.ViewFamily.Drafting:
                    if not destViewType in self._destViewTypes:
                        self._destViewTypes.append(destViewType)
                        
        def _setup(self):
            button_name = "Transfer"
            self.select_b.Content = button_name

            self.multiselect = True
            self.views_dg.SelectionMode = Controls.DataGridSelectionMode.Extended

            self.ctx_groups_title = "View Type"
            self.ctx_groups_title_tb.Text = self.ctx_groups_title

            self.ctx_groups_active = None
            
            self.sort_groups = "sorted"
            
            self._prepare_context()

            self.hide_element(self.clrsearch_b)

            self.search_tb.TextChanged += self.search_txt_changed
            self.ctx_groups_selector_cb.SelectionChanged += self.selection_changed

            self.clear_search(None, None)
            
            self.views_dg.Items.Refresh()

        def _prepare_context_items(self, ctx_items):
            new_ctx = []

            for item in ctx_items:
                if isinstance(item, self._parent.ViewSelectorFormItem):
                    item.checkable = self.multiselect
                    new_ctx.append(item)
                else:
                    new_ctx.append(self._parent.ViewSelectorFormItem(self._parent, item, checked=False,
                                                                     checkable=self.multiselect, replace=False, replacable=False))

            return new_ctx

        @staticmethod
        def _natural_sort_key(key):
            return [int(c) if c.isdigit() else c.lower() for c in re.split(r'(\d+)', key)]

        def _prepare_context(self):
            if isinstance(self._context, dict) and self._context.keys():
                if self.sort_groups == "sorted":
                    sorted_groups = sorted(self._context.keys())
                elif self.sort_groups == "natural":
                    sorted_groups = sorted(self._context.keys(), key=self._natural_sort_key)
                else:
                    sorted_groups = self._context.keys()
                
                self._update_ctx_groups(sorted_groups)
                
                new_ctx = OrderedDict()
                for ctx_grp in sorted_groups:
                    items = self._prepare_context_items(self._context[ctx_grp])
                    new_ctx[ctx_grp] = items

                self._context = new_ctx
            else:
                self._context = self._prepare_context_items(self._context)

        def _update_ctx_groups(self, ctx_group_names):
            self.show_element(self.ctx_groups_dock)
            self.ctx_groups_selector_cb.ItemsSource = ctx_group_names
            if self.ctx_groups_active in ctx_group_names:
                self.ctx_groups_selector_cb.SelectedIndex = ctx_group_names.index(self.ctx_groups_active)
            else:
                self.ctx_groups_selector_cb.SelectedIndex = 0

        def _get_active_ctx_group(self):
            return self.ctx_groups_selector_cb.SelectedItem

        def _get_active_ctx(self):
            if isinstance(self._context, dict):
                return self._context[self._get_active_ctx_group()]
            else:
                return self._context

        def _list_options(self, option_filter=None):
            if option_filter:
                fuzzy_matches = sorted(
                    [(x,
                      coreutils.fuzzy_search_ratio(
                          target_string=x.view_name,
                          sfilter=option_filter,
                          regex=False))
                     for x in self._get_active_ctx()],
                    key=lambda x: x[1],
                    reverse=True
                    )
                self.views_dg.ItemsSource = ObservableCollection[self._parent.ViewSelectorFormItem]([x[0] for x in fuzzy_matches if x[1] >= 80])
            else:
                self.views_dg.ItemsSource = ObservableCollection[self._parent.ViewSelectorFormItem](self._get_active_ctx())
                
        def button_select(self, sender, args):
            matching_views = []
            
            for k in self._context:
                if isinstance(self._context, dict):
                    for v in self._context[k]:
                        if v.checked or v in self.views_dg.SelectedItems:
                            matching_view = next((x for x in DB.FilteredElementCollector(self._doc).OfClass(DB.View) if
                                                  x.Name == str(v.view_name)), None)
                            v.replacable = True
                            
                            if matching_view is not None and not v.replace:
                                matching_views.append(v)
                            else:
                                self._viewsToTransfer.append(v)
                else:
                    if k.checked or k in self.views_dg.SelectedItems:
                        matching_view = next((x for x in DB.FilteredElementCollector(self._doc).OfClass(DB.View) if
                                              x.Name == str(k.view_name)), None)
                        k.replacable = True
                        
                        if matching_view is not None and not k.replace:
                            matching_views.append(k)
                        else:
                            self._viewsToTransfer.append(k)
                                 
            self.hide_element(self.ctx_groups_dock)
            self.ctx_groups_selector_cb.SelectedIndex = 0
            
            self._context = matching_views
            self._list_options()

            if len(matching_views) == 0 and len(self._viewsToTransfer) > 0:
                for v in self._viewsToTransfer:
                    self._update_dest_types()

                    if str(v.view_type) != "Retain Original":
                        destType = next((x for x in self._destViewTypes if revit.query.get_name(x) == v.view_type), None)
                    else:
                        targetTypeName = revit.query.get_name(revit.query.get_type(v.unwrap()))

                        destType = next((x for x in self._destViewTypes if revit.query.get_name(x) == targetTypeName), None)
                        if destType is None:
                            try:
                                with revit.Transaction("DB Tools - Copying View Type", doc=self._doc, clear_after_rollback=True, swallow_errors=True, log_errors=False):
                                    destType = self._destViewTypes[0].Duplicate(targetTypeName)
                            except:
                                pass
                                
                    if destType is None:
                        forms.alert("View could not retain original type: " + v.view_name + ". Setting to default ('Detail')...", title="DB Tools")
                        destType = next((x for x in self._destViewTypes if revit.query.get_name(x) == "Detail"), None)
                        
                    try:
                        with revit.Transaction("DB Tools - Transfer View", doc=self._doc, clear_after_rollback=True, swallow_errors=True, log_errors=False):
                            self._result = self._parent.transfer_view(v.unwrap(), v.view_name, destType, v.replace)
                        
                            if self._result and not v.replace:
                                self._numAdded += 1
                            elif self._result and v.replace:
                                self._numReplaced += 1
                    except:
                        forms.alert("View failed to transfer: " + v.view_name + ". Contining to next view...", title="DB Tools")
                        continue
                    
                self.Close()
            else:
                forms.alert("Views remaining in list need to be renamed or set to be replaced before transfer.", title="DB Tools")

        def search_txt_changed(self, sender, args):
            if self.search_tb.Text == '':
                self.hide_element(self.clrsearch_b)
            else:
                self.show_element(self.clrsearch_b)

            self._list_options(option_filter=self.search_tb.Text)

        def selection_changed(self, sender, args):
            self._list_options(option_filter=self.search_tb.Text)

        def selected_item_changed(self, sender, args):
            self.view_i.RenderTransform = Media.MatrixTransform(self._reset)
            match = next((x for x in self._draftingViews if x.Name == getattr(self.views_dg.SelectedItem, "Name", None)), None)

            if match is not None:
                self.show_element(self.view_i)
                file_name = DB.ImageExportOptions.GetFileName(self._targetDoc, match.Id)
                file_match = next((x for x in os.listdir(self._temp_dir) if str(file_name) in str(x)), None)

                if file_match is None:
                    originalBG = self._targetDoc.Application.BackgroundColor
                    self._targetDoc.Application.BackgroundColor = DB.Color(255, 255, 255)
                    
                    img = DB.ImageExportOptions()
                    img.ZoomType = DB.ZoomFitType.FitToPage
                    img.PixelSize = 1024
                    img.ImageResolution = DB.ImageResolution.DPI_150
                    img.FitDirection = DB.FitDirectionType.Horizontal
                    img.ExportRange = DB.ExportRange.SetOfViews
                    img.HLRandWFViewsFileType = DB.ImageFileType.PNG
                    img.FilePath = os.path.join(self._temp_dir, "preview.png")
                    img.ShouldCreateWebSite = False
                    img.ShadowViewsFileType = DB.ImageFileType.PNG
                    img.SetViewsAndSheets([match.Id])

                    self._targetDoc.ExportImage(img)
                    
                    self._targetDoc.Application.BackgroundColor = originalBG
                    
                file_match = next((x for x in os.listdir(self._temp_dir) if str(file_name) in str(x)), None)

                self.set_image_source(self.view_i, os.path.join(self._temp_dir, file_match))
            else:
                self.hide_element(self.view_i)
                
        def clear_search(self, sender, args):
            self.search_tb.Text = " "
            self.search_tb.Clear()
            self.search_tb.Focus()
            
        def view_zoom(self, sender, args):
            p = args.MouseDevice.GetPosition(self.view_i)
            m = self.view_i.RenderTransform.Value

            if (args.Delta > 0):
                m.ScaleAtPrepend(1.1, 1.1, p.X, p.Y)
            else:
                m.ScaleAtPrepend(1/1.1, 1/1.1, p.X, p.Y)

            self.view_i.RenderTransform = Media.MatrixTransform(m)
            
        def view_reset(self, sender, args):
            self.view_i.RenderTransform = Media.MatrixTransform(self._reset)
            
        def view_pan_start(self, sender, args):
            if self.view_i.IsMouseCaptured is False:
                self.view_i.CaptureMouse()
                self._start = args.GetPosition(self.view_border)
                self._origin.X = self.view_i.RenderTransform.Value.OffsetX
                self._origin.Y = self.view_i.RenderTransform.Value.OffsetY

        def view_pan_stop(self, sender, args):
             self.view_i.ReleaseMouseCapture()
             
        def view_pan_move(self, sender, args):
            if self.view_i.IsMouseCaptured is not False:
                p = args.MouseDevice.GetPosition(self.view_border)

                m = self.view_i.RenderTransform.Value
                m.OffsetX = self._origin.X + (p.X - self._start.X)
                m.OffsetY = self._origin.Y + (p.Y - self._start.Y)

                self.view_i.RenderTransform = Media.MatrixTransform(m)
             
        @property
        def result(self):
            return self._result

        @property
        def numAdded(self):
            return self._numAdded

        @property
        def numReplaced(self):
            return self._numReplaced

    def copy_elements(self, source, dest, elements):
        class CustomCopyHandler(DB.IDuplicateTypeNamesHandler):
            def OnDuplicateTypeNamesFound(self, args):
                return DB.DuplicateTypeAction.UseDestinationTypes

        transform = DB.Transform.Identity
        cpOptions = DB.CopyPasteOptions()
        cpOptions.SetDuplicateTypeNamesHandler(CustomCopyHandler())

        new_element_ids = DB.ElementTransformUtils.CopyElements(source, elements, dest, transform, cpOptions)

        return new_element_ids

    def clear_view_contents(self, doc, view):
        viewElements = DB.FilteredElementCollector(doc, view.Id)
        viewElementsCollection = List[DB.ElementId]()

        for element in viewElements:  
                viewElementsCollection.Add(element.Id)

        doc.Delete(viewElementsCollection)

    def transfer_view(self, copyView, newViewName, newViewType, replace=False):
        if replace:
            newViews = [x for x in DB.FilteredElementCollector(self._doc).OfClass(DB.View).WhereElementIsNotElementType().ToElements()
                                   if x.ViewType == DB.ViewType.DraftingView]
            newView = next((x for x in newViews if x.Name == newViewName), None)

            if newView is not None:
                self.clear_view_contents(self._doc, newView)
            else:
                forms.alert("View was set to replace: " + newViewName + " however no matching view was found. Transferring as new instead.", title="DB Tools")

        newView = DB.ViewDrafting.Create(self._doc, newViewType.Id)

        newView.Name = newViewName
        newView.Scale = copyView.Scale
        newView.Parameter[DB.BuiltInParameter.VIEW_DESCRIPTION].Set(copyView.Parameter[DB.BuiltInParameter.VIEW_DESCRIPTION].AsString())
        
        filter = DB.ElementCategoryFilter(DB.ElementId.InvalidElementId, True)
        copyElements = DB.FilteredElementCollector(self._targetDoc, copyView.Id).WherePasses(filter).WhereElementIsNotElementType()
        copyElementsCollection = List[DB.ElementId]()

        for element in copyElements:
            if (element.Category and element.Category.Name == "Title Blocks"):
                continue
            elif isinstance(element, DB.ScheduleSheetInstance):
                continue
            if isinstance(element, DB.Viewport) or "ExtentElem" in element.Name:
                continue
            elif isinstance(element, DB.Element) and element.Category and "guide" in str(element.Category.Name).lower():
                continue
            elif isinstance(element, DB.Element) and element.Category and "views" == str(element.Category.Name).lower():
                continue
            else:
                copyElementsCollection.Add(element.Id)

        if copyElements.GetElementCount() > 0:
            newElementsCollection = self.copy_elements(copyView, newView, copyElementsCollection)

            for dest, src in zip(newElementsCollection, copyElementsCollection):
                try:
                    newView.SetElementOverrides(dest, copyView.GetElementOverrides(src))
                except:
                    continue

        return True
    
    @property
    def numAdded(self):
        return self._viewSelectorForm.numAdded

    @property
    def numReplaced(self):
        return self._viewSelectorForm.numReplaced

script.py (for importing)

from DBTools import *

from pyrevit import HOST_APP
from pyrevit import script

import ConfigParser
import codecs

app = HOST_APP.uiapp.Application
doc = revit.doc
uidoc = revit.uidoc

logger = script.get_logger()

search_dir = os.path.normpath(r"network_drive\Structural\Drawing Tools\Revit\Details")

steel = MasterDetailFile("Steel")
concrete = MasterDetailFile("Concrete")
masonry = MasterDetailFile("Masonry")
wood = MasterDetailFile("Wood")
cold_form = MasterDetailFile("Cold Form")

master_files = [steel, concrete, masonry, wood, cold_form]

numAdded = 0
numReplaced = 0

REVIT_YEAR = app.VersionNumber

if str(REVIT_YEAR) == "2024":
    REVIT_YEAR = "R24"
else:
    forms.alert("Revit year " + str(REVIT_YEAR) + " is not supported.", title="DB Tools", exitscript=True)
    
for master in master_files:
    files = [x for x in os.listdir(os.path.join(search_dir, master.name)) if x.endswith(REVIT_YEAR + ".rvt")]

    if len(files) == 0:
        forms.alert("Unable to locate " + master.name + " master library file.")
        continue
    
    if len(files) > 1:
        forms.alert("Multiple matching .rvt files found in " + master.name + " directory. Using most recently modified file found.", title="DB Tools")
        files.sort(key=os.path.getmtime)

    master.path = os.path.join(search_dir, master.name, files[0])

dest_master_file = None
dest_master_file = forms.SelectFromList.show(master_files, title="Select Destination", name_attr="name", multiselect=False)

if dest_master_file is not None:
    targetDoc = None
    
    try:
        ini_path = os.path.join(app.CurrentUsersDataFolderPath, "Revit.ini")
        ini_config = ConfigParser.RawConfigParser()
        ini_config.readfp(codecs.open(ini_path, "r", "utf-16-le"))
        
        local_path = os.path.join(
                        os.path.expandvars(ini_config.get("Directories", "ProjectPath")),
                        os.path.splitext(os.path.basename(dest_master_file.path))[0] + "_" + str(app.Username) + ".rvt")

        if os.path.exists(local_path):
            os.remove(local_path)
            
        local_model_path = DB.ModelPathUtils.ConvertUserVisiblePathToModelPath(local_path)
        central_model_path = DB.ModelPathUtils.ConvertUserVisiblePathToModelPath(dest_master_file.path)        
        DB.WorksharingUtils.CreateNewLocal(central_model_path, local_model_path)

        openConfig = DB.WorksetConfiguration(DB.WorksetConfigurationOption.OpenAllWorksets)
        openOptions = DB.OpenOptions()
        openOptions.DetachFromCentralOption = DB.DetachFromCentralOption.DoNotDetach
        openOptions.Audit = False
        openOptions.AllowOpeningLocalByWrongUser = True
        openOptions.SetOpenWorksetsConfiguration(openConfig)

        targetDoc = app.OpenDocumentFile(local_model_path, openOptions)
    except:
        forms.alert("Unable to open selected master for editing...", title="DB Tools", exitscript=True)      

    transfer = TransferDraftingViews(doc, targetDoc, "Import?")

    targetDoc.Close(False)

    if transfer.numAdded != 0 and transfer.numReplaced != 0:
        forms.alert("Successfully imported ["+ str(transfer.numAdded) + "] details and replaced [" + str(transfer.numReplaced) + "] details...", title="DB Tools")

The last code block above for importing can be easily modified for exporting since the overall WPF code is based upon just transferring views.

Let me know if anyone has any comments or criticisms! 

AK