Ofir
Ofir

Reputation: 5279

Implement file explorer based on tree view with multiple selection

I`m beginner WPF. I developing a new project at my work and I need to insert a file explorer control with multiple selection.

The concept need to be similar to acronis file explorer: (Treeview with checkboxes)

See Example

Look at the left container, I need to implement something similar to this, I habe searched alot through google and I saw lot of implementations but nothing wasn`t similar to this.

Because I don`t have alot experience in WPF it quite difficult for me to start.

Do you have some tips or similar projects which might help me do it?

My project based on MVVM DP.

Thanks

Upvotes: 1

Views: 4888

Answers (2)

user1618054
user1618054

Reputation:

After taking a look at the answer by @AlSki, I decided it is neither intuitive nor versatile enough for my liking and came up with my own solution. The disadvantage of using my solution, however, is it requires a tad bit more boilerplate. On the other hand, it offers a LOT more flexibility.

The samples below assume you use .NET 4.6.1 and C# 6.0.

/// <summary>
/// A base for abstract objects (implements INotifyPropertyChanged).
/// </summary>
[Serializable]
public abstract class AbstractObject : INotifyPropertyChanged
{
    /// <summary>
    /// 
    /// </summary>
    [field: NonSerialized()]
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// 
    /// </summary>
    /// <param name="propertyName"></param>
    public void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    /// <summary>
    /// 
    /// </summary>
    /// <typeparam name="TKind"></typeparam>
    /// <param name="Source"></param>
    /// <param name="NewValue"></param>
    /// <param name="Names"></param>
    protected virtual bool SetValue<TKind>(ref TKind Source, TKind NewValue, params string[] Notify)
    {
        //Set value if the new value is different from the old
        if (!Source.Equals(NewValue))
        {
            Source = NewValue;

            //Notify all applicable properties
            Notify?.ForEach(i => OnPropertyChanged(i));

            return true;
        }

        return false;
    }

    /// <summary>
    /// 
    /// </summary>
    public AbstractObject()
    {
    }
}

An object with a check state.

/// <summary>
/// Specifies an object with a checked state.
/// </summary>
public interface ICheckable
{
    /// <summary>
    /// 
    /// </summary>
    bool? IsChecked
    {
        get; set;
    }
}

/// <summary>
/// 
/// </summary>
public class CheckableObject : AbstractObject, ICheckable
{
    /// <summary>
    /// 
    /// </summary>
    [field: NonSerialized()]
    public event EventHandler<EventArgs> Checked;

    /// <summary>
    /// 
    /// </summary>
    [field: NonSerialized()]
    public event EventHandler<EventArgs> Unchecked;

    /// <summary>
    /// 
    /// </summary>
    [XmlIgnore]
    protected bool? isChecked;
    /// <summary>
    /// 
    /// </summary>
    public virtual bool? IsChecked
    {
        get
        {
            return isChecked;
        }
        set
        {
            if (SetValue(ref isChecked, value, "IsChecked") && value != null)
            {
                if (value.Value)
                {
                    OnChecked();
                }
                else OnUnchecked();
            }
        }
    }

    /// <summary>
    /// 
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return base.ToString();
        //return isChecked.ToString();
    }

    /// <summary>
    /// 
    /// </summary>
    protected virtual void OnChecked()
    {
        Checked?.Invoke(this, new EventArgs());
    }

    /// <summary>
    /// 
    /// </summary>
    protected virtual void OnUnchecked()
    {
        Unchecked?.Invoke(this, new EventArgs());
    }

    /// <summary>
    /// 
    /// </summary>
    public CheckableObject() : base()
    {
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="isChecked"></param>
    public CheckableObject(bool isChecked = false)
    {
        IsChecked = isChecked;
    }
}

The view model for checked system objects:

/// <summary>
/// 
/// </summary>
public class CheckableSystemObject : CheckableObject
{
    #region Properties

    /// <summary>
    /// 
    /// </summary>
    public event EventHandler Collapsed;

    /// <summary>
    /// 
    /// </summary>
    public event EventHandler Expanded;

    bool StateChangeHandled = false;

    CheckableSystemObject Parent { get; set; } = default(CheckableSystemObject);

    ISystemProvider SystemProvider { get; set; } = default(ISystemProvider);

    ConcurrentCollection<CheckableSystemObject> children = new ConcurrentCollection<CheckableSystemObject>();
    /// <summary>
    /// 
    /// </summary>
    public ConcurrentCollection<CheckableSystemObject> Children
    {
        get
        {
            return children;
        }
        private set
        {
            SetValue(ref children, value, "Children");
        }
    }

    bool isExpanded = false;
    /// <summary>
    /// 
    /// </summary>
    public bool IsExpanded
    {
        get
        {
            return isExpanded;
        }
        set
        {
            if (SetValue(ref isExpanded, value, "IsExpanded"))
            {
                if (value)
                {
                    OnExpanded();
                }
                else OnCollapsed();
            }
        }
    }

    bool isSelected = false;
    /// <summary>
    /// 
    /// </summary>
    public bool IsSelected
    {
        get
        {
            return isSelected;
        }
        set
        {
            SetValue(ref isSelected, value, "IsSelected");
        }
    }

    string path = string.Empty;
    /// <summary>
    /// 
    /// </summary>
    public string Path
    {
        get
        {
            return path;
        }
        set
        {
            SetValue(ref path, value, "Path");
        }
    }

    bool queryOnExpanded = false;
    /// <summary>
    /// 
    /// </summary>
    public bool QueryOnExpanded
    {
        get
        {
            return queryOnExpanded;
        }
        set
        {
            SetValue(ref queryOnExpanded, value);
        }
    }

    /// <summary>
    /// 
    /// </summary>
    public override bool? IsChecked
    {
        get
        {
            return isChecked;
        }
        set
        {
            if (SetValue(ref isChecked, value, "IsChecked") && value != null)
            {
                if (value.Value)
                {
                    OnChecked();
                }
                else OnUnchecked();
            }
        }
    }

    #endregion

    #region CheckableSystemObject

    /// <summary>
    /// 
    /// </summary>
    /// <param name="path"></param>
    /// <param name="systemProvider"></param>
    /// <param name="isChecked"></param>
    public CheckableSystemObject(string path, ISystemProvider systemProvider, bool? isChecked = false) : base()
    {
        Path = path;
        SystemProvider = systemProvider;
        IsChecked = isChecked;
    }

    #endregion

    #region Methods

    void Determine()
    {
        //If it has a parent, determine it's state by enumerating all children, but current instance, which is already accounted for.
        if (Parent != null)
        {
            StateChangeHandled = true;
            var p = Parent;
            while (p != null)
            {
                p.IsChecked = Determine(p);
                p = p.Parent;
            }
            StateChangeHandled = false;
        }
    }

    bool? Determine(CheckableSystemObject Root)
    {
        //Whether or not all children and all children's children have the same value
        var Uniform = true;

        //If uniform, the value
        var Result = default(bool?);

        var j = false;
        foreach (var i in Root.Children)
        {
            //Get first child's state
            if (j == false)
            {
                Result = i.IsChecked;
                j = true;
            }
            //If the previous child's state is not equal to the current child's state, it is not uniform and we are done!
            else if (Result != i.IsChecked)
            {
                Uniform = false;
                break;
            }
        }

        return !Uniform ? null : Result;
    }

    void Query(ISystemProvider SystemProvider)
    {
        children.Clear();
        if (SystemProvider != null)
        {
            foreach (var i in SystemProvider.Query(path))
            {
                children.Add(new CheckableSystemObject(i, SystemProvider, isChecked)
                {
                    Parent = this
                });
            }
        }
    }

    /// <summary>
    /// 
    /// </summary>
    protected override void OnChecked()
    {
        base.OnChecked();

        if (!StateChangeHandled)
        {
            //By checking the root only, all children are checked automatically
            foreach (var i in children)
                i.IsChecked = true;

            Determine();
        }
    }

    /// <summary>
    /// 
    /// </summary>
    protected override void OnUnchecked()
    {
        base.OnUnchecked();

        if (!StateChangeHandled)
        {
            //By unchecking the root only, all children are unchecked automatically
            foreach (var i in children)
                i.IsChecked = false;

            Determine();
        }
    }

    /// <summary>
    /// 
    /// </summary>
    protected virtual void OnCollapsed()
    {
        Collapsed?.Invoke(this, new EventArgs());
    }

    /// <summary>
    /// 
    /// </summary>
    protected virtual void OnExpanded()
    {
        Expanded?.Invoke(this, new EventArgs());

        if (!children.Any<CheckableSystemObject>() || queryOnExpanded)
            BeginQuery(SystemProvider);
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="SystemProvider"></param>
    public async void BeginQuery(ISystemProvider SystemProvider)
    {
        await Task.Run(() => Query(SystemProvider));
    }

    #endregion
}

Utilities for querying system objects; note, by defining your own SystemProvider, you can query different types of systems (i.e., local or remote). By default, your local system is queried. If you want to display objects from a remote server like FTP, you'd want to define a SystemProvider that utilizes the appropriate web protocol.

/// <summary>
/// Specifies an object capable of querying system objects.
/// </summary>
public interface ISystemProvider
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="Path">The path to query.</param>
    /// <param name="Source">A source used to make queries.</param>
    /// <returns>A list of system object paths.</returns>
    IEnumerable<string> Query(string Path, object Source = null);
}

/// <summary>
/// Defines base functionality for an <see cref="ISystemProvider"/>.
/// </summary>
public abstract class SystemProvider : ISystemProvider
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="Path"></param>
    /// <param name="Source"></param>
    /// <returns></returns>
    public abstract IEnumerable<string> Query(string Path, object Source = null);
}

/// <summary>
/// Defines functionality to query a local system.
/// </summary>
public class LocalSystemProvider : SystemProvider
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="Path"></param>
    /// <param name="Source"></param>
    /// <returns></returns>
    public override IEnumerable<string> Query(string Path, object Source = null)
    {
        if (Path.IsNullOrEmpty())
        {
            foreach (var i in System.IO.DriveInfo.GetDrives())
                yield return i.Name;
        }
        else
        {
            if (System.IO.Directory.Exists(Path))
            {
                foreach (var i in System.IO.Directory.EnumerateFileSystemEntries(Path))
                    yield return i;
            }
        }
    }
}

And then an inherited TreeView, which puts this all together:

/// <summary>
/// 
/// </summary>
public class SystemObjectPicker : TreeViewExt
{
    #region Properties

    /// <summary>
    /// 
    /// </summary>
    public static DependencyProperty QueryOnExpandedProperty = DependencyProperty.Register("QueryOnExpanded", typeof(bool), typeof(SystemObjectPicker), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnQueryOnExpandedChanged));
    /// <summary>
    /// 
    /// </summary>
    public bool QueryOnExpanded
    {
        get
        {
            return (bool)GetValue(QueryOnExpandedProperty);
        }
        set
        {
            SetValue(QueryOnExpandedProperty, value);
        }
    }
    static void OnQueryOnExpandedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        d.As<SystemObjectPicker>().OnQueryOnExpandedChanged((bool)e.NewValue);
    }

    /// <summary>
    /// 
    /// </summary>
    public static DependencyProperty RootProperty = DependencyProperty.Register("Root", typeof(string), typeof(SystemObjectPicker), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnRootChanged));
    /// <summary>
    /// 
    /// </summary>
    public string Root
    {
        get
        {
            return (string)GetValue(RootProperty);
        }
        set
        {
            SetValue(RootProperty, value);
        }
    }
    static void OnRootChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        d.As<SystemObjectPicker>().OnRootChanged((string)e.NewValue);
    }

    /// <summary>
    /// 
    /// </summary>
    static DependencyProperty SystemObjectsProperty = DependencyProperty.Register("SystemObjects", typeof(ConcurrentCollection<CheckableSystemObject>), typeof(SystemObjectPicker), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    /// <summary>
    /// 
    /// </summary>
    ConcurrentCollection<CheckableSystemObject> SystemObjects
    {
        get
        {
            return (ConcurrentCollection<CheckableSystemObject>)GetValue(SystemObjectsProperty);
        }
        set
        {
            SetValue(SystemObjectsProperty, value);
        }
    }

    /// <summary>
    /// 
    /// </summary>
    public static DependencyProperty SystemProviderProperty = DependencyProperty.Register("SystemProvider", typeof(ISystemProvider), typeof(SystemObjectPicker), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSystemProviderChanged));
    /// <summary>
    /// 
    /// </summary>
    public ISystemProvider SystemProvider
    {
        get
        {
            return (ISystemProvider)GetValue(SystemProviderProperty);
        }
        set
        {
            SetValue(SystemProviderProperty, value);
        }
    }
    static void OnSystemProviderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        d.As<SystemObjectPicker>().OnSystemProviderChanged((ISystemProvider)e.NewValue);
    }

    #endregion

    #region SystemObjectPicker

    /// <summary>
    /// 
    /// </summary>
    public SystemObjectPicker() : base()
    {
        SetCurrentValue(SystemObjectsProperty, new ConcurrentCollection<CheckableSystemObject>());
        SetCurrentValue(SystemProviderProperty, new LocalSystemProvider());

        SetBinding(ItemsSourceProperty, new Binding()
        {
            Mode = BindingMode.OneWay,
            Path = new PropertyPath("SystemObjects"),
            Source = this
        });
    }

    #endregion

    #region Methods

    void OnQueryOnExpandedChanged(CheckableSystemObject Item, bool Value)
    {
        foreach (var i in Item.Children)
        {
            i.QueryOnExpanded = Value;
            OnQueryOnExpandedChanged(i, Value);
        }
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="Value"></param>
    protected virtual void OnQueryOnExpandedChanged(bool Value)
    {
        foreach (var i in SystemObjects)
            OnQueryOnExpandedChanged(i, Value);
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="Provider"></param>
    /// <param name="Root"></param>
    protected virtual void OnRefreshed(ISystemProvider Provider, string Root)
    {
        SystemObjects.Clear();
        if (Provider != null)
        {
            foreach (var i in Provider.Query(Root))
            {
                SystemObjects.Add(new CheckableSystemObject(i, SystemProvider)
                {
                    QueryOnExpanded = QueryOnExpanded
                });
            }
        }
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="Value"></param>
    protected virtual void OnRootChanged(string Value)
    {
        OnRefreshed(SystemProvider, Value);
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="Value"></param>
    protected virtual void OnSystemProviderChanged(ISystemProvider Value)
    {
        OnRefreshed(Value, Root);
    }

    #endregion
}

Obviously, it's dramatically more complex than @AlSki's answer, but, again, you get more flexibility and the hard stuff is taken care of for you already.

In addition, I have published this code in the latest version of my open source project (3.1) if such a thing interests you; if not, the samples above is all you need to get it working.

If you do not download the project, note the following:

  • You will find some extension methods that do not exist, which can be supplemented with their counterparts (e.g., IsNullOrEmpty extension is identical to string.IsNullOrEmpty()).
  • TreeViewExt is a custom TreeView I designed so if you don't care about that, simply change TreeViewExt to TreeView; either way, you should not have to define a special control template for it as it was designed to work with TreeView's existing facilities.
  • In the sample, I use my own version of a concurrent ObservableCollection; this is so you can query data on a background thread without jumping through hoops. Change this to ObservableCollection and make all queries synchronous OR use your own concurrent ObservableCollection to preserve the asynchronous functionality.

Finally, here is how you would use the control:

<Controls.Extended:SystemObjectPicker>
    <Controls.Extended:SystemObjectPicker.ItemContainerStyle>
        <Style TargetType="TreeViewItem" BasedOn="{StaticResource {x:Type TreeViewItem}}">
            <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
        </Style>
    </Controls.Extended:SystemObjectPicker.ItemContainerStyle>
    <Controls.Extended:SystemObjectPicker.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding Children, Mode=OneWay}">
            <StackPanel Orientation="Horizontal">
                <CheckBox
                    IsChecked="{Binding IsChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                    Margin="0,0,5,0"/>
                <TextBlock 
                    Text="{Binding Path, Converter={StaticResource FileNameConverter}, Mode=OneWay}"/>
            </StackPanel>
        </HierarchicalDataTemplate>
    </Controls.Extended:SystemObjectPicker.ItemTemplate>
</Controls.Extended:SystemObjectPicker>

To Do

  • Add a property to CheckableSystemObject that exposes a view model for the system object; that way, you can access the FileInfo/DirectoryInfo associated with it's path or some other source of data that otherwise describes it. If the object is remote, you may have already defined your own class to describe it, which could be useful if you have the reference to it.
  • Catch possible exceptions when querying a local system; if a system object cannot be accessed, it will fail. LocalSystemProvider also fails to address system paths that exceed 260 characters; however, that is beyond the scope of this project.

Note To Moderators

I referenced my own open source project for convenience as I published the above samples in the latest version; my intention is not to self-promote so if referencing your own project is frowned upon, I will proceed to remove the link.

Upvotes: 1

AlSki
AlSki

Reputation: 6961

Remodelling the Treeview is very easy, you start with your collection that you want to bind to, i.e.

<Grid>
  <TreeView ItemsSource="{Binding Folders}"/>
</Grid>

However you then need to define how to display the data you have bound to. I'm assuming that your items are just an IEnumerable (any list or array) of FolderViewModels and FileViewModels (both have a Name property), so now we need to say how to display those. You do that by defining a DataTemplate and since this is for a tree we use a HeirarchicalDataTemplate as that also defines subItems

<Grid.Resources>
  <HierarchicalDataTemplate DataType="{x:Type viewModel:FolderViewModel}"
                              ItemsSource="{Binding SubFoldersAndFiles}">
        <CheckBox Content="{Binding Name}"/>
  </HierarchicalDataTemplate>
<Grid.Resources/>

Files are the same but dont need sub items

<HierarchicalDataTemplate DataType="{x:Type viewModel:FolderViewModel}">
   <CheckBox Content="{Binding Name}"/>
</HierarchicalDataTemplate>

So putting it all together you get

<Grid>
    <Grid.Resources>
        <HierarchicalDataTemplate DataType="{x:Type viewModel:FolderViewModel}"
                              ItemsSource="{Binding SubFoldersAndFiles}">
            <CheckBox Content="{Binding Name}"/>
       </HierarchicalDataTemplate>
       <HierarchicalDataTemplate DataType="{x:Type viewModel:FolderViewModel}">
            <CheckBox Content="{Binding Name}"/>
      </HierarchicalDataTemplate>
    <Grid.Resources/>
    <TreeView ItemsSource="{Binding Folders}"/>
</Grid>

Icons If you want to show icons then you change the content in the CheckBox, I'm assuming you will define an Image on your ViewModel.

        <CheckBox>
            <CheckBox.Content>
                <StackPanel Orientation="Horizontal">
                    <Image Source="{Binding Image}"/>
                    <TextBlock Text="{Binding Name}"/>
                </StackPanel>
            </CheckBox.Content>

Selection

Finally you have to handle the selection of items. I'd advise adding an IsSelected property to your FileViewModel and FolderViewModels. For files this is incredibly simple, its just a bool.

 public class FileViewModel : INotifyProperty
   ...
   public bool IsSelected //Something here to handle get/set and NotifyPropertyChanged that depends on your MVVM framework, I use ReactiveUI a lot so that's this syntax
   { 
      get { return _IsSelected;}
      set { this.RaiseAndSetIfChanged(x=>x.IsSelected, value); }
   }

and

<CheckBox IsChecked="{Binding IsSelected}">

Its slightly more complicated with FolderViewModel and I'll look at the logic in a second. First the Xaml, just replace the current CheckBox declaration with

<CheckBox IsThreeState="True" IsChecked="{Binding IsSelected}">
    <!--IsChecked = True, False or null-->

So now we need to return a set of Nullable<bool> (aka bool?).

public bool? IsSelected
{
   get
   { 
      if (SubFoldersAndFiles.All(x=>x.IsSelected) return true;
      if (SubFoldersAndFiles.All(x=>x.IsSelected==false) return false;
      return null;
   }
   set
   {
      // We can't set to indeterminate at folder level so we have to set to 
      // set to oposite of what we have now
      if(value == null)
         value = !IsSelected;

      foreach(var x in SubFoldersAndFiles)
          x.IsSelected = value;
   }

Or something very similar...

Upvotes: 5

Related Questions