mirror of https://github.com/dahall/Vanara.git
Added initial Computer class with support for shares (more to come!).
parent
b1e35497d2
commit
8dc4e9562b
|
@ -0,0 +1,172 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Vanara
|
||||
{
|
||||
/// <summary>Represents a single connected (authenticated) computer.</summary>
|
||||
/// <seealso cref="System.ComponentModel.Component"/>
|
||||
/// <seealso cref="System.ComponentModel.ISupportInitialize"/>
|
||||
/// <seealso cref="System.Runtime.Serialization.ISerializable"/>
|
||||
[Serializable, DefaultProperty(nameof(Target))]
|
||||
public class Computer : Component, ISupportInitialize, ISerializable
|
||||
{
|
||||
/// <summary>The local computer connected by the current account.</summary>
|
||||
public static readonly Computer Local = new Computer();
|
||||
|
||||
private bool initializing;
|
||||
private string targetServer;
|
||||
private bool targetServerSet;
|
||||
private string userName;
|
||||
private bool userNameSet;
|
||||
private string userPassword;
|
||||
private bool userPasswordSet;
|
||||
private SharedDevices devices;
|
||||
|
||||
/// <summary>Creates a new instance of a TaskService connecting to the local machine as the current user.</summary>
|
||||
public Computer() => Connect();
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="Computer"/> class.</summary>
|
||||
/// <param name="target">
|
||||
/// The name of the computer that you want to connect to. If the this parameter is <see langword="null"/>, then this will connect to
|
||||
/// the local computer.
|
||||
/// </param>
|
||||
/// <param name="userName">
|
||||
/// The user name that is used during the connection to the computer. If the user is not specified, then the current user is used.
|
||||
/// </param>
|
||||
/// <param name="password">
|
||||
/// The password that is used to connect to the computer. If the user name and password are not specified, then the current token is used.
|
||||
/// </param>
|
||||
public Computer(string target, string userName = null, string accountDomain = null, string password = null)
|
||||
{
|
||||
BeginInit();
|
||||
Target = target;
|
||||
UserName = userName;
|
||||
UserPassword = password;
|
||||
EndInit();
|
||||
}
|
||||
|
||||
private Computer(SerializationInfo info, StreamingContext context)
|
||||
{
|
||||
BeginInit();
|
||||
Target = (string)info.GetValue(nameof(Target), typeof(string));
|
||||
UserName = (string)info.GetValue(nameof(UserName), typeof(string));
|
||||
UserPassword = (string)info.GetValue(nameof(UserPassword), typeof(string));
|
||||
EndInit();
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the name of the computer that the user is connected to.</summary>
|
||||
[Category("Data"), DefaultValue(null), Description("The name of the computer to connect to.")]
|
||||
public string Target
|
||||
{
|
||||
get => ShouldSerializeTargetServer() ? targetServer : null;
|
||||
set
|
||||
{
|
||||
if (value == null || value.Trim() == string.Empty) value = null;
|
||||
if (string.Compare(value, targetServer, StringComparison.OrdinalIgnoreCase) != 0)
|
||||
{
|
||||
targetServerSet = true;
|
||||
targetServer = value;
|
||||
Connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the shared devices defined for this computer.</summary>
|
||||
/// <value>Returns a <see cref="SharedDevices"/> value.</value>
|
||||
[Browsable(false)]
|
||||
public SharedDevices SharedDevices => devices ?? (devices = new SharedDevices(this));
|
||||
|
||||
//public IEnumerable<LocalUser> LocalUsers => LocalUser.GetEnum(Target, UserName, UserPassword);
|
||||
|
||||
//public IEnumerable<LocalGroup> LocalGroups => LocalGroup.GetEnum(Target, UserName, UserPassword);
|
||||
|
||||
/// <summary>Gets or sets the user name to be used when connecting to the <see cref="Target"/>.</summary>
|
||||
/// <value>The user name.</value>
|
||||
[Category("Data"), DefaultValue(null), Description("The user name to be used when connecting.")]
|
||||
public string UserName
|
||||
{
|
||||
get => ShouldSerializeUserName() ? userName : null;
|
||||
set
|
||||
{
|
||||
if (value == null || value.Trim() == string.Empty) value = null;
|
||||
if (string.Compare(value, userName, StringComparison.OrdinalIgnoreCase) != 0)
|
||||
{
|
||||
userNameSet = true;
|
||||
userName = value;
|
||||
Connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the user password to be used when connecting to the <see cref="Target"/>.</summary>
|
||||
/// <value>The user password.</value>
|
||||
[Category("Data"), DefaultValue(null), Description("The user password to be used when connecting.")]
|
||||
public string UserPassword
|
||||
{
|
||||
get => userPassword;
|
||||
set
|
||||
{
|
||||
if (value == null || value.Trim() == string.Empty) value = null;
|
||||
if (string.CompareOrdinal(value, userPassword) != 0)
|
||||
{
|
||||
userPasswordSet = true;
|
||||
userPassword = value;
|
||||
Connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Signals the object that initialization is starting.</summary>
|
||||
public void BeginInit() => initializing = true;
|
||||
|
||||
/// <summary>Signals the object that initialization is complete.</summary>
|
||||
public void EndInit()
|
||||
{
|
||||
initializing = false;
|
||||
Connect();
|
||||
}
|
||||
|
||||
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
|
||||
{
|
||||
info.AddValue(nameof(Target), Target, typeof(string));
|
||||
info.AddValue(nameof(UserName), UserName, typeof(string));
|
||||
info.AddValue(nameof(UserPassword), UserPassword, typeof(string));
|
||||
}
|
||||
|
||||
private void Connect()
|
||||
{
|
||||
ResetUnsetProperties();
|
||||
|
||||
if (!initializing && !DesignMode)
|
||||
{
|
||||
// Clear stuff if already connected
|
||||
Dispose(true);
|
||||
|
||||
if (!string.IsNullOrEmpty(targetServer))
|
||||
{
|
||||
// Check to ensure character only server name. (Suggested by bigsan)
|
||||
if (targetServer.StartsWith(@"\"))
|
||||
targetServer = targetServer.TrimStart('\\');
|
||||
// Make sure null is provided for local machine to compensate for a native library oddity (Found by ctrollen)
|
||||
if (targetServer.Equals(Environment.MachineName, StringComparison.CurrentCultureIgnoreCase))
|
||||
targetServer = null;
|
||||
}
|
||||
else
|
||||
targetServer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetUnsetProperties()
|
||||
{
|
||||
if (!targetServerSet) targetServer = null;
|
||||
if (!userNameSet) userName = null;
|
||||
if (!userPasswordSet) userPassword = null;
|
||||
}
|
||||
|
||||
private bool ShouldSerializeTargetServer() => targetServer != null && !targetServer.Trim('\\').Equals(Environment.MachineName.Trim('\\'), StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
private bool ShouldSerializeUserName() => userName != null && !userName.Equals(Environment.UserName, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
namespace Vanara
|
||||
{
|
||||
/// <summary>An object that exposes a name.</summary>
|
||||
public interface INamedEntity
|
||||
{
|
||||
string Name { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,258 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Security.AccessControl;
|
||||
using Vanara.Extensions;
|
||||
using static Vanara.PInvoke.NetApi32;
|
||||
|
||||
namespace Vanara
|
||||
{
|
||||
/// <summary>Offline settings for a shared folder.</summary>
|
||||
public enum ShareOfflineSettings
|
||||
{
|
||||
/// <summary>Only the files and programs that users specify are available offline.</summary>
|
||||
OnlySpecified = SHI1005_FLAGS.CSC_CACHE_MANUAL_REINT,
|
||||
|
||||
/// <summary>All files and programs that users open from the shared folder are automatically available offline.</summary>
|
||||
All = SHI1005_FLAGS.CSC_CACHE_AUTO_REINT,
|
||||
|
||||
/// <summary>
|
||||
/// All files and programs that users open from the shared folder are automatically available offline and are cached for performance.
|
||||
/// </summary>
|
||||
AllOptimized = SHI1005_FLAGS.CSC_CACHE_VDO,
|
||||
|
||||
/// <summary>No files or programs from the shared folder are available offline.</summary>
|
||||
None = SHI1005_FLAGS.CSC_CACHE_NONE,
|
||||
}
|
||||
|
||||
/// <summary>Represents a shared device on a computer.</summary>
|
||||
/// <seealso cref="Vanara.INamedEntity"/>
|
||||
public class SharedDevice : INamedEntity
|
||||
{
|
||||
private readonly string target;
|
||||
private STYPE type = (STYPE)uint.MaxValue;
|
||||
|
||||
internal SharedDevice(string target, string netname)
|
||||
{
|
||||
this.target = target;
|
||||
Name = netname;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets an optional comment about the shared resource.</summary>
|
||||
/// <value>The resource description.</value>
|
||||
public string Description
|
||||
{
|
||||
get => NetShareGetInfo<SHARE_INFO_1>(target, Name).shi1_remark;
|
||||
set => NetShareSetInfo(target, Name, new SHARE_INFO_1004 { shi1004_remark = value });
|
||||
}
|
||||
|
||||
/// <summary>Gets a value indicating whether this instance is communication device.</summary>
|
||||
/// <value><see langword="true"/> if this instance is communication device; otherwise, <see langword="false"/>.</value>
|
||||
public bool IsCommDevice => (Type & STYPE.STYPE_MASK) == STYPE.STYPE_DEVICE;
|
||||
|
||||
/// <summary>Gets a value indicating whether this instance is disk drive.</summary>
|
||||
/// <value><see langword="true"/> if this instance is disk drive; otherwise, <see langword="false"/>.</value>
|
||||
public bool IsDiskVolume => (Type & STYPE.STYPE_MASK) == STYPE.STYPE_DISKTREE;
|
||||
|
||||
/// <summary>Gets a value indicating whether this instance is Interprocess Communication.</summary>
|
||||
/// <value><see langword="true"/> if this instance is Interprocess Communication; otherwise, <see langword="false"/>.</value>
|
||||
public bool IsIPC => (Type & STYPE.STYPE_MASK) == STYPE.STYPE_IPC;
|
||||
|
||||
/// <summary>Gets a value indicating whether this instance is a print queue.</summary>
|
||||
/// <value><see langword="true"/> if this instance is a print queue; otherwise, <see langword="false"/>.</value>
|
||||
public bool IsPrintQueue => (Type & STYPE.STYPE_MASK) == STYPE.STYPE_PRINTQ;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating a special share reserved for interprocess communication (IPC$) or remote administration of the server
|
||||
/// (ADMIN$). Can also refer to administrative shares such as C$, D$, E$, and so forth.
|
||||
/// </summary>
|
||||
/// <value><see langword="true"/> if this instance is special; otherwise, <see langword="false"/>.</value>
|
||||
public bool IsSpecial => Type.IsFlagSet(STYPE.STYPE_SPECIAL);
|
||||
|
||||
/// <summary>Gets a value indicating whether this instance is temporary.</summary>
|
||||
/// <value><see langword="true"/> if this instance is temporary; otherwise, <see langword="false"/>.</value>
|
||||
public bool IsTemporary => Type.IsFlagSet(STYPE.STYPE_TEMPORARY);
|
||||
|
||||
/// <summary>Gets the share name of a resource.</summary>
|
||||
/// <value>Returns a <see cref="string"/> value.</value>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>Gets or sets the offline settings associated with a disk volume share.</summary>
|
||||
/// <value>The offline settings.</value>
|
||||
public ShareOfflineSettings OfflineSettings
|
||||
{
|
||||
get => (ShareOfflineSettings)(NetShareGetInfo<SHARE_INFO_1005>(target, Name).shi1005_flags & SHI1005_FLAGS.CSC_MASK_EXT);
|
||||
set
|
||||
{
|
||||
var i = NetShareGetInfo<SHARE_INFO_1005>(target, Name);
|
||||
i.shi1005_flags = i.shi1005_flags & ~SHI1005_FLAGS.CSC_MASK | (SHI1005_FLAGS)value;
|
||||
NetShareSetInfo(target, Name, i);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the local path for the shared resource. For disks, this is the path being shared. For print queues, this is the name
|
||||
/// of the print queue being shared.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Returns a <see cref="string"/> value. If the caller does not have rights to get this information, this property returns <see cref="string.Empty"/>.
|
||||
/// </value>
|
||||
public string Path
|
||||
{
|
||||
get
|
||||
{
|
||||
try { return NetShareGetInfo<SHARE_INFO_2>(target, Name).shi2_path; } catch { return string.Empty; }
|
||||
}
|
||||
set
|
||||
{
|
||||
var i = NetShareGetInfo<SHARE_INFO_2>(target, Name);
|
||||
i.shi2_path = value;
|
||||
NetShareSetInfo(target, Name, i);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the permissions of the shared resource.</summary>
|
||||
/// <value>
|
||||
/// The access permissions for the share. If the caller does not have rights to get this information, this property returns <see langword="null"/>.
|
||||
/// </value>
|
||||
public RawSecurityDescriptor Permissions
|
||||
{
|
||||
get
|
||||
{
|
||||
try { return NetShareGetInfo<SHARE_INFO_502>(target, Name).shi502_security_descriptor.ToManaged(); } catch { return null; }
|
||||
}
|
||||
set
|
||||
{
|
||||
var i = NetShareGetInfo<SHARE_INFO_502>(target, Name);
|
||||
i.shi502_security_descriptor = value.ToNative();
|
||||
NetShareSetInfo(target, Name, i);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of concurrent connections that the shared resource can accommodate. The number of connections is
|
||||
/// unlimited if the value specified in this member is –1.
|
||||
/// </summary>
|
||||
/// <value>The maximum number of concurrent connections.</value>
|
||||
public int UserLimit
|
||||
{
|
||||
get
|
||||
{
|
||||
try { return unchecked((int)NetShareGetInfo<SHARE_INFO_2>(target, Name).shi2_max_uses); } catch { return -1; }
|
||||
}
|
||||
set
|
||||
{
|
||||
var i = NetShareGetInfo<SHARE_INFO_2>(target, Name);
|
||||
i.shi2_max_uses = unchecked((uint)value);
|
||||
NetShareSetInfo(target, Name, i);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the shared resource's permissions for servers running with share-level security.</summary>
|
||||
/// <value>Returns a <see cref="ShareLevelAccess"/> value.</value>
|
||||
private ShareLevelAccess Access => NetShareGetInfo<SHARE_INFO_2>(target, Name).shi2_permissions;
|
||||
|
||||
private STYPE Type => (uint)type == uint.MaxValue ? (type = NetShareGetInfo<SHARE_INFO_1>(target, Name).shi1_type) : type;
|
||||
|
||||
/// <summary>Creates the disk volume share.</summary>
|
||||
/// <param name="target">
|
||||
/// A string that specifies the DNS or NetBIOS name of the remote server on which the function is to execute. If this parameter is
|
||||
/// <see langword="null"/>, the local computer is used.
|
||||
/// </param>
|
||||
/// <param name="name">The share name of a resource.</param>
|
||||
/// <param name="comment">An optional comment about the shared resource.</param>
|
||||
/// <param name="path">
|
||||
/// The local path for the shared resource. For disks, this is the path being shared. For print queues, this is the name of the print
|
||||
/// queue being shared.
|
||||
/// </param>
|
||||
/// <returns>On success, a new instance of <see cref="SharedDevice"/> represented a newly created shared disk.</returns>
|
||||
public static SharedDevice CreateDiskVolumeShare(string target, string name, string comment, string path) =>
|
||||
Create(target, name, comment, path, STYPE.STYPE_DISKTREE);
|
||||
|
||||
/// <summary>Creates the specified target.</summary>
|
||||
/// <param name="target">
|
||||
/// A string that specifies the DNS or NetBIOS name of the remote server on which the function is to execute. If this parameter is
|
||||
/// <see langword="null"/>, the local computer is used.
|
||||
/// </param>
|
||||
/// <param name="name">The share name of a resource.</param>
|
||||
/// <param name="comment">An optional comment about the shared resource.</param>
|
||||
/// <param name="path">
|
||||
/// The local path for the shared resource. For disks, this is the path being shared. For print queues, this is the name of the print
|
||||
/// queue being shared.
|
||||
/// </param>
|
||||
/// <param name="type">A combination of values that specify the type of the shared resource.</param>
|
||||
/// <returns>On success, a new instance of <see cref="SharedDevice"/> represented a newly created shared resource.</returns>
|
||||
internal static SharedDevice Create(string target, string name, string comment, string path, STYPE type)
|
||||
{
|
||||
NetShareAdd(target, new SHARE_INFO_2 { shi2_netname = name, shi2_remark = comment, shi2_path = path, shi2_max_uses = unchecked((uint)-1), shi2_type = type });
|
||||
return new SharedDevice(target, name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Represents all the shared devices on a computers.</summary>
|
||||
/// <seealso cref="Vanara.NamedEntityDictionary{Vanara.SharedDevice}"/>
|
||||
public class SharedDevices : Collections.VirtualDictionary<string, SharedDevice>
|
||||
{
|
||||
private readonly string target = null;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="SharedDevices"/> class.</summary>
|
||||
/// <param name="serverName">Name of the computer from which to retrieve and manage the shared devices.</param>
|
||||
public SharedDevices(string serverName = null) : base(false) => target = serverName;
|
||||
|
||||
internal SharedDevices(Computer computer) : this(computer.Target)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Gets the number of elements contained in the <see cref="T:System.Collections.Generic.IDictionary`2"/>.</summary>
|
||||
/// <value>The number of elements contained in the <see cref="T:System.Collections.Generic.IDictionary`2"/>.</value>
|
||||
public override int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
var h = 0U;
|
||||
NetShareEnum(target, 0, out var _, MAX_PREFERRED_LENGTH, out var cnt, out var _, ref h).ThrowIfFailed();
|
||||
return (int)cnt;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets an <see cref="T:System.Collections.Generic.ICollection`1"/> containing the keys of the <see cref="T:System.Collections.Generic.IDictionary`2"/>.</summary>
|
||||
/// <value>An <see cref="T:System.Collections.Generic.ICollection`1"/> containing the keys of the object that implements <see cref="T:System.Collections.Generic.IDictionary`2"/>.</value>
|
||||
public override ICollection<string> Keys => NetShareEnum<SHARE_INFO_0>(target).Select(i => i.shi0_netname).ToArray();
|
||||
|
||||
/// <summary>Creates the specified target.</summary>
|
||||
/// <param name="name">The share name of a resource.</param>
|
||||
/// <param name="comment">An optional comment about the shared resource.</param>
|
||||
/// <param name="path">
|
||||
/// The local path for the shared resource. For disks, this is the path being shared. For print queues, this is the name of the print
|
||||
/// queue being shared.
|
||||
/// </param>
|
||||
/// <param name="type">A combination of values that specify the type of the shared resource.</param>
|
||||
/// <returns>On success, a new instance of <see cref="SharedDevice"/> represented a newly created shared resource.</returns>
|
||||
public SharedDevice Add(string name, string comment, string path, STYPE type = STYPE.STYPE_DISKTREE) => SharedDevice.Create(target, name, comment, path, type);
|
||||
|
||||
/// <summary>Removes the element with the specified key from the <see cref="T:System.Collections.Generic.IDictionary`2"/>.</summary>
|
||||
/// <param name="key">The key of the element to remove.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the element is successfully removed; otherwise, <see langword="false"/>. This method also returns false
|
||||
/// if key was not found in the original <see cref="T:System.Collections.Generic.IDictionary`2"/>.
|
||||
/// </returns>
|
||||
public override bool Remove(string key) => NetShareDel(target, key).Succeeded;
|
||||
|
||||
/// <summary>Gets the value associated with the specified key.</summary>
|
||||
/// <param name="key">The key whose value to get.</param>
|
||||
/// <param name="value">
|
||||
/// When this method returns, the value associated with the specified key, if the key is found; otherwise, the default value for the
|
||||
/// type of the <paramref name="value"/> parameter. This parameter is passed uninitialized.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the <see cref="T:System.Collections.Generic.IDictionary`2"/> contains an element with the key;
|
||||
/// otherwise, <see langword="false"/>.
|
||||
/// </returns>
|
||||
public override bool TryGetValue(string key, out SharedDevice value)
|
||||
{
|
||||
value = ContainsKey(key) ? new SharedDevice(target, key) : null;
|
||||
return !(value is null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -60,6 +60,16 @@ BackgroundCopyACLFlags, BackgroundCopyCost, BackgroundCopyErrorContext, Backgrou
|
|||
<Reference Include="System.ServiceProcess" />
|
||||
<Reference Include="System" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Computer\LocalGroup.cs" />
|
||||
<Compile Remove="Computer\LocalUser.cs" />
|
||||
<Compile Remove="Computer\~LocalGroup.cs" />
|
||||
<Compile Remove="Computer\~LocalUser.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Computer\~LocalGroup.cs" />
|
||||
<None Include="Computer\~LocalUser.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="4.5.0" Condition=" '$(TargetFramework)' == 'netstandard2.0' Or '$(TargetFramework)' == 'netcoreapp2.0' Or '$(TargetFramework)' == 'netcoreapp2.1' " />
|
||||
<PackageReference Include="Theraot.Core" Version="3.0.2" Condition=" '$(TargetFramework)' == 'net20' Or '$(TargetFramework)' == 'net35' Or '$(TargetFramework)' == 'net40' " />
|
||||
|
@ -68,6 +78,7 @@ BackgroundCopyACLFlags, BackgroundCopyCost, BackgroundCopyErrorContext, Backgrou
|
|||
<ProjectReference Include="..\PInvoke\BITS\Vanara.PInvoke.BITS.csproj" />
|
||||
<ProjectReference Include="..\PInvoke\IpHlpApi\Vanara.PInvoke.IpHlpApi.csproj" />
|
||||
<ProjectReference Include="..\PInvoke\Kernel32\Vanara.PInvoke.Kernel32.csproj" />
|
||||
<ProjectReference Include="..\PInvoke\NetApi32\Vanara.PInvoke.NetApi32.csproj" />
|
||||
<ProjectReference Include="..\PInvoke\NetListMgr\Vanara.PInvoke.NetListMgr.csproj" />
|
||||
<ProjectReference Include="..\PInvoke\ShlwApi\Vanara.PInvoke.ShlwApi.csproj" />
|
||||
<ProjectReference Include="..\PInvoke\User32\Vanara.PInvoke.User32.csproj" />
|
||||
|
|
Loading…
Reference in New Issue