jeudi 15 juin 2023

WPF treeview checkboxes how to check all children when parent is checked

Yes, i know its been asked a bazillion times, I've looked through, and tried most of those answers. for context, I'm creating a dock-able pane within an API (Revit). Most solutions are based on the Windows class, but I'm forced to use a Page class for the form. My data is gathered from two lists parsed through a custom class and sorted into one list, then i had the brilliant idea of mapping it to a dictionary, with one of the list properties as its key. Displaying the dictionary in a treeview was nice an easy, but as you can see checking the Parent does not check all its children: Populated TreeView

So id like to keep the dictionary if possible, but am fully prepared to restructure my list as needed to get this working. To summarize the ask:

  • Keep the data structured as as a Dictionory if possible.
  • Be able to check all children when parent is checked, but parent should not be checked (i do not require a three state) when children are checked.
  • Checkbox and item are separate controls, such that user can select an item, which will display and image without checking the checkbox.

What i've tried; recursive functions and Revit do not mix, so mapping my list to the treeview that way is out. Binding the IsChecked of the child to the parent and vice versa. I think this fails due to the way the xaml is structured to use the dictionary. And a slew of other things i cant think of right now.

Anyways, heres the XAML:

<Page x:Class="Typ_Detail_Selector.TypDetForm"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d" 
             Title="Typical Detail Selector" Foreground="{x:Null}" MinWidth="100" MinHeight="400" Background="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}">
    <StackPanel HorizontalAlignment="Stretch" Margin="5,5,5,5" VerticalAlignment="Stretch">
        <Label Content="Views" HorizontalAlignment="Left" Margin="0,0,0,0" VerticalAlignment="Top"/>
        <TreeView x:Name="TreeViewDets" HorizontalAlignment="Left" Height="400" MinWidth="400" SelectedItemChanged="TreeViewDets_SelectedItemChanged">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Value}">
                    <StackPanel Orientation="Horizontal">
                        <CheckBox Focusable="False" IsChecked="{Binding Checked}" Click="CheckPleaseParent"/>
                        <TextBlock Text="{Binding Key}"/>
                    </StackPanel>
                    <HierarchicalDataTemplate.ItemTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Horizontal">
                                <CheckBox Focusable="False" IsChecked="{Binding Checked}" Click="CheckPleaseChild"/>
                                <TextBlock Text="{Binding Name}"/>
                            </StackPanel>
                        </DataTemplate>
                    </HierarchicalDataTemplate.ItemTemplate>
                    <HierarchicalDataTemplate.ItemContainerStyle>
                        <Style TargetType = "TreeViewItem">
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding Path=Loaded}" Value="true">
                                    <Setter Property="Focusable" Value="False"/>
                                    <Setter Property="Foreground" Value="DimGray"/>
                                </DataTrigger>
                                <MultiDataTrigger>
                                    <MultiDataTrigger.Conditions>
                                        <Condition Binding="{Binding Path=Loaded}" Value="false"/>
                                        <Condition Binding="{Binding Path=Checked}" Value="true"/>
                                    </MultiDataTrigger.Conditions>
                                    <Setter Property="Background" Value="SteelBlue"/>
                                    <Setter Property="Foreground" Value="Azure"/>
                                </MultiDataTrigger>
                            </Style.Triggers>
                        </Style>
                    </HierarchicalDataTemplate.ItemContainerStyle>
                    <HierarchicalDataTemplate.Resources>
                        <Style TargetType="{x:Type CheckBox}">
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding Path=Loaded}" Value="true">
                                    <Setter Property="IsEnabled" Value="False"/>
                                    <Setter Property="Foreground" Value="DimGray"/>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </HierarchicalDataTemplate.Resources>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
        <Grid>
            <Button x:Name="RunButton" Content="Insert Views" HorizontalAlignment="Right"  VerticalAlignment="Bottom" Width="180" Height="25" Click="Run_Click"/>
        </Grid>
    </StackPanel>
</Page>

And the xaml.cs, i must stress this is for API work, so theres a lot of extra things not specific to the WPF itself:

using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media.Imaging;
using System.Windows.Controls;
using Autodesk.Revit.UI;
using Autodesk.Revit.DB;
using Autodesk.Revit.DB.Events;
using View = Autodesk.Revit.DB.View;
using Application = Autodesk.Revit.ApplicationServices.Application;
using Transform = Autodesk.Revit.DB.Transform;
using System.Reflection;
using System.Collections;

namespace Typ_Detail_Selector
{
    public partial class TypDetForm : Page, IDockablePaneProvider
    {
        private TypDetailRegister TypDetViews;
        public Document TypDoc;
        private Document ThisDoc;
        private Application App;
        private UIApplication UiApp;
        private Dictionary<string, List<ViewProperties>> viewDict = new Dictionary<string, List<ViewProperties>>();

        public TypDetForm()
        {
            InitializeComponent();
        }
        public void SetupDockablePane(DockablePaneProviderData data)
        {
            data.FrameworkElement = this;
            data.InitialState = new DockablePaneState();
            data.InitialState.DockPosition = DockPosition.Right;
        }
        public void ReferenceItems(TypDetailRegister _typdetviews, Document _typdoc, Document _thisdoc, Application _app)
        {   //this is to set references to things from command class
            TypDoc = _typdoc;
            ThisDoc = _thisdoc;
            TypDetViews = _typdetviews;
            App = _app;
            UiApp = new UIApplication(App);
            UiApp.Idling += TypDetViews.TwiddleThumbs;
            viewDict = TypDetViews.ViewList().GroupBy(x => x.SheetName).ToDictionary(x => x.Key, x => x.ToList());
            RefreshCompare();
        }
        public void RefreshCompare()
        {//compares list of views an schedules in the current project to the typ det file,
         //flags them as "loaded" so you cant re import them later.
            List<ViewProperties> _tempviewprops = TypDetViews.ViewList();
            List<Element> _compareViewList = new FilteredElementCollector(ThisDoc).OfClass(typeof(ViewDrafting))
                .Where(v => v.Name != "MKA Starting View").ToList();
            List<Element> _compareSchedList = new FilteredElementCollector(ThisDoc).OfClass(typeof(ViewSchedule))
                .Where(s => !s.Name.ToUpper().Contains("REVISION"))
                .Where(s => !s.Name.Contains("DRAWING LIST"))
                .ToList();
            _compareViewList.AddRange(_compareSchedList);
            foreach (ViewProperties v in TypDetViews.ViewList())
            {
                var _viewExists = from views in _compareViewList
                                  where v.Name == views.Name
                                  select v;
                if (_viewExists.Any())
                {
                    v.Loaded = true;
                    v.Checked = true;
                }
                else
                {
                    v.Loaded = false;
                    v.Checked = false;
                }
            }

            TreeViewDets.ItemsSource = null;
            TreeViewDets.ItemsSource = viewDict;
        }
      
        private void ListViewDets_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {   //handler for when user selects (but not checks!) and item in teh list
            string imagelibrary = @"K:\Development\_Typical Detail Change Tracking\Base Models\Typ Images";
            if (e.AddedItems.Count > 0) //check if something was in fact selected
            {
                //dim teh item as a member of the list class to get at certain properites
                ViewProperties test = e.AddedItems[0] as ViewProperties;
                try
                {
                    //replace characters that windows automatically replaces for the png files
                    string view_image = imagelibrary + "\\" + test.Name.Replace("\"", "-").Replace("/", "-") + ".png";
                    if (File.Exists(view_image))//if an image file exists in teh library
                    { //add it to to image control on the form as preview.
                        BitmapImage bmp = new BitmapImage(new Uri(view_image, UriKind.Absolute));
                        // PreviewImage.Source = bmp;
                    }
                }
                catch { }
                e.AddedItems.Clear();//clear it so program does not get confused the next time handler is triggered.
            }
        }

        private void CheckPleaseChild(object sender, RoutedEventArgs e)
        { //seperate handler to change the checkbox, and ping the selection changed event
            var cb = sender as CheckBox;
            var item = cb.DataContext;
            ListViewDets.SelectedItem = item;
        }
        private void CheckPleaseParent(object sender, RoutedEventArgs e)
        { //seperate handler to change the checkbox, and ping the selection changed event
            var cb = sender as CheckBox;
            var iteam = cb.DataContext;
            PropertyInfo[] testprops = cb.DataContext.GetType().GetProperties();
            List<ViewProperties> testlist = new List<ViewProperties>();
            testlist = (List<ViewProperties>)testprops.Last().GetValue(cb.DataContext, null);
            foreach (var temp in testlist)
            {
                //need to trigger checkpleasechild somehow?

            }

        }
        private void TreeViewDets_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
        {
            TreeView tv = sender as TreeView;
            var item = tv.Items.CurrentItem; //this gets me the KVP, same as in checkpleaseparent
        }
        private void ListViewDets_SizeChanged(object sender, SizeChangedEventArgs e)
        {   //this is to stretch the listview with the form
            ListView tempview = sender as ListView;
            GridView gview = tempview.View as GridView;
            var defaultwidth = tempview.ActualWidth - SystemParameters.VerticalScrollBarWidth;
            var percent = 0.95;
            gview.Columns[1].Width = defaultwidth * percent;
        }
        private void Run_Click(object sender, RoutedEventArgs e)
        {   //clicking run, sets the bool, which in trun runs the InsertViews when able (idling)
            GlobalVars.DoTheThings = true;
        }
        public void InsertViews(ExternalCommandData commdata)
        {
            GlobalVars.DoTheThings = false;//reset bool
            App.FailuresProcessing += HideDupeMessages;//to deal with dupilcate names dialog
            ExternalCommandData _commdata = commdata;
            //grab all the element ids of items that are checked in the list, but not loaded, which are the views we want to insert
            ICollection<ElementId> _viewIds = TypDetViews.ViewList().Where(x => x.Checked && !x.Loaded).Select(y => y.ViewId).ToList();
            ICollection<ElementId> _copiedViewIds = null;
            Document _doc = _commdata.Application.ActiveUIDocument.Document;
            ThisDoc = _doc;
            using (TransactionGroup tg = new TransactionGroup(_doc)) //trans group to keep the undo list clean
            {
                tg.Start();
                using (Transaction trView = new Transaction(_doc))
                {
                    foreach (ElementId _viewId in _viewIds)
                    {
                        ICollection<ElementId> _vId = _viewIds.Where(x => x == _viewId).ToList(); //annoying API thing, must put the element id in an Icollection, else it doesn work
                        bool test = TypDetViews.ViewList().Where(x => x.ViewId == _vId.First()).Select(y => y.SchedTest).First();//is it a scehdule?
                        trView.Start("Copy Typ Views to Project");
                        {
                            CopyPasteOptions opts = new CopyPasteOptions();
                            opts.SetDuplicateTypeNamesHandler(new DupeNamesHandler());
                            try
                            {   //add the elementid of the view or sched that was just inserted to a empty collection
                                _copiedViewIds = ElementTransformUtils.CopyElements(TypDoc, _vId, _doc, Transform.Identity, opts);
                            }
                            catch { }
                            trView.Commit();
                        }
                        //if its not a sched, grab every element in teh view,
                        //bypass for scheds, as it grabs the modeled elements in the sched(errors out for stickies)
                        if (test)
                        {
                            ICollection<ElementId> _nestedEelemIds = new FilteredElementCollector(TypDoc, _viewId)
                                .WherePasses(new ElementCategoryFilter(ElementId.InvalidElementId, true))
                                .ToElementIds();
                            //if we foudn elements, copy them from the typ view to the new view we just inserted
                            if (_nestedEelemIds.Count > 0)
                            {
                                View _sourceView = TypDoc.GetElement(_viewId) as View;
                                View _targetView = _doc.GetElement(_copiedViewIds.First()) as View;
                                using (Transaction trElements = new Transaction(_doc))
                                {
                                    trElements.Start("Copy Typ Det Elements to Project View");
                                    CopyPasteOptions opts = new CopyPasteOptions();
                                    opts.SetDuplicateTypeNamesHandler(new DupeNamesHandler());
                                    ElementTransformUtils.CopyElements(_sourceView, _nestedEelemIds, _targetView, Transform.Identity, opts);
                                    trElements.Commit();
                                }
                            }
                        }
                        _copiedViewIds.Clear();//clear teh list for the next run.
                    }
                }
                tg.Assimilate();
            }

            //write to a text file
            try
            {
                string log_file = "Typical Detail Selector.txt";
                string text = "";
                string date = DateTime.Now.Year.ToString() + "." + DateTime.Now.Month.ToString() + "." + DateTime.Now.Day.ToString();
                string user = Environment.UserName;
                string other = "Run";
                text = date + " ~ " + user + " ~ " + other;
                StreamWriter sw = File.AppendText("J:/CADD/Customization/Add-In Usage/" + log_file);
                sw.WriteLine(text);
                sw.Close();
            }
            catch
            {

            }
            RefreshCompare();//refresh the lsit when we done inserting views.
        }
        public void SetActiveDocument()
        {
            ThisDoc = UiApp.ActiveUIDocument.Document;
            RefreshCompare();
        }
        public void NewDocCity(object sender, DocumentOpenedEventArgs e)
        {
            SetActiveDocument();
        }
       
        //public PreviewControl PrevCon { get; set; }
        private void HideDupeMessages(object sender, FailuresProcessingEventArgs e)
        {
            FailuresAccessor FA = e.GetFailuresAccessor();
            foreach (FailureMessageAccessor fmsg in FA.GetFailureMessages())
            {
                if (fmsg.GetSeverity() == FailureSeverity.Warning)
                {
                    FA.DeleteWarning(fmsg);
                    e.SetProcessingResult(FailureProcessingResult.ProceedWithCommit);
                }
            }
            e.SetProcessingResult(FailureProcessingResult.Continue);
        }
    }
    public class DupeNamesHandler : IDuplicateTypeNamesHandler
    {
        public DuplicateTypeAction OnDuplicateTypeNamesFound(DuplicateTypeNamesHandlerArgs args)
        {
            return DuplicateTypeAction.UseDestinationTypes;
        }
    }
}



Aucun commentaire:

Enregistrer un commentaire