Closing Excel Application Process in C# after Data Access
Asked Answered
U

22

102

I'm writing an application in C# that opens an Excel template file for read/write operations. I want to when user closes the application, excel application process has been closed, without saving excel file. See my Task Manager after multiple runs of the app.

enter image description here

I use this code to open the excel file :

public Excel.Application excelApp = new Excel.Application();
public Excel.Workbook excelBook;
excelBook = excelApp.Workbooks.Add(@"C:/pape.xltx");

and for data access I use this code :

Excel.Worksheet excelSheet = (Worksheet)(excelBook.Worksheets[1]);
excelSheet.DisplayRightToLeft = true;
Range rng;
rng = excelSheet.get_Range("C2");
rng.Value2 = txtName.Text;

I see similar questions in stackoverflow such as this question and this, and test answers, but it doesn't works.

Unmoved answered 21/7, 2013 at 22:20 Comment(1)
Excel and Word are very slow and come with a ton of quirks like the one you've stumbled upon. The files are zip files with some XML and other content in there. There is also Open XML SDK (or maybe something more recent) that can open the documents. Such code works without Office installed locally too. Consider not using ExcelDemarche
P
103

Try this:

excelBook.Close(0); 
excelApp.Quit();

When closing the work-book, you have three optional parameters:

Workbook.close SaveChanges, filename, routeworkbook 

Workbook.Close(false) or if you are doing late binding, it sometimes is easier to use zero Workbook.Close(0) That is how I've done it when automating closing of workbooks.

Also I went and looked up the documentation for it, and found it here: Excel Workbook Close

Pinhole answered 21/7, 2013 at 23:3 Comment(7)
Thanks Dear Michael, It's works properly : excelBook.Close(0)Unmoved
Hi Guys this is not working when you are opening an excel file for reading. Following is the code var excelApp = new Application(); var workBooks = excelApp.Workbooks.Open@"c:\temp\myexcel.xlsx"); workBooks.Close(0); excelApp.Quit();Liberati
I have used applicationClass... and i used the above code to dispose the objects but it wont works ...Gardel
After many failed attempts with all these answers, I went with this solution to get the process ID that I opened and kill it directly.Wakayama
@Singaravelan: Have you checked David Clarke's answer?Wenn
God bless you!!Gaur
Or more clearly: Workbook.Close(SaveChanges: false). You can also open the Excel file in read-only mode: Workbooks.Open(Filename: excelFile, UpdateLinks: false, ReadOnly: true). However, this still won't likely resolve the issue of having leftover Excel processes. You'll need to call Marshal.FinalReleaseComObject along with GC.Collect and GC.WaitForPendingFinalizers to fix that.Clytemnestra
C
26
xlBook.Save();
xlBook.Close(true);
xlApp.Quit();
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlApp);

try this.. it worked for me... you should release that xl application object to stop the process.

Clamatorial answered 26/11, 2013 at 9:23 Comment(1)
I had to release my workbook and my worksheet as well to get the process to stop. I am using powershell, so it looks like [System.Runtime.InteropServices.Marshal]::ReleaseComObject($xlBook) instead. The process stopped after about 4 seconds.Milne
G
17

Ref: https://mcmap.net/q/212173/-cannot-close-excel-exe-after-interop-process

Avoid using double-dot-calling expressions, such as this:

var workbook = excel.Workbooks.Open(/*params*/)

...because in this way you create RCW objects not only for workbook, but for Workbooks, and you should release it too (which is not possible if a reference to the object is not maintained).

This resolved the issue for me. Your code becomes:

public Excel.Application excelApp = new Excel.Application();
public Excel.Workbooks workbooks;
public Excel.Workbook excelBook;
workbooks = excelApp.Workbooks;
excelBook = workbooks.Add(@"C:/pape.xltx");

...

Excel.Sheets sheets = excelBook.Worksheets;
Excel.Worksheet excelSheet = (Worksheet)(sheets[1]);
excelSheet.DisplayRightToLeft = true;
Range rng;
rng = excelSheet.get_Range("C2");
rng.Value2 = txtName.Text;

And then release all those objects:

System.Runtime.InteropServices.Marshal.ReleaseComObject(rng);
System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet);
System.Runtime.InteropServices.Marshal.ReleaseComObject(sheets);
excelBook .Save();
excelBook .Close(true);
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlBook);
System.Runtime.InteropServices.Marshal.ReleaseComObject(workbooks);
excelApp.Quit();
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlApp);

I wrap this in a try {} finally {} to ensure everything gets released even if something goes wrong (what could possibly go wrong?) e.g.

public Excel.Application excelApp = null;
public Excel.Workbooks workbooks = null;
...
try
{
    excelApp = new Excel.Application();
    workbooks = excelApp.Workbooks;
    ...
}
finally
{
    ...
    if (workbooks != null) System.Runtime.InteropServices.Marshal.ReleaseComObject(workbooks);
    excelApp.Quit();
    System.Runtime.InteropServices.Marshal.ReleaseComObject(xlApp);
}
Goring answered 22/1, 2015 at 2:24 Comment(3)
This has worked for me, assigning workbooks to a variable so that it can be cleared. Also one observation, I had exactly the same code for clearing the excel app in Web application and console application. The code before the update of getting rid of double dot worked fine in the console app but when run in the web app it did not clear the EXCEL.EXE. I am not sure why they behaved differently but getting rid of the double dot reference fixed it in the web app.Boeschen
working for me. I think that to releasecomobject, we must release all related object. I had same problem about solidworks program.Niacin
Close the workbook, quit Excel application, avoid double-dot-calling, and release every single COM object created. That worked for me. Life saving answer. Thanks.Greave
F
14

Think of this, it kills the process:

System.Diagnostics.Process[] process=System.Diagnostics.Process.GetProcessesByName("Excel");
foreach (System.Diagnostics.Process p in process)
{
    if (!string.IsNullOrEmpty(p.ProcessName))
    {
        try
        {
            p.Kill();
        }
        catch { }
    }
}

Also, did you try just close it normally?

myWorkbook.SaveAs(@"C:/pape.xltx", missing, missing, missing, missing, missing, Microsoft.Office.Interop.Excel.XlSaveAsAccessMode.xlNoChange, missing, missing, missing, missing, missing);
excelBook.Close(null, null, null);                 // close your workbook
excelApp.Quit();                                   // exit excel application
excel = null;                                      // set to NULL
Franciscofranciska answered 21/7, 2013 at 22:27 Comment(5)
Thanks for your answer.Your first solution closes All open Excel files.when I use 'excelBook.Close', excel save dialog appears, and I don't want it :(Unmoved
I edited it and added it with a line to do the saveAs in code which will replace the dialog by code, hope it helps.:)Franciscofranciska
I just used this with an initial check for open Excel processes and saved the IDs in a list. Then made a new Excel process, did the editing I had to and closed any Excel process that wasn't contained in the ID list.Indirection
It is not the correct way to release excel from the task manager. You should release all the objects that were created including those one which were created behind the scene such as WorkBOoks.Oquassa
This way, you'd kill every single running Excel process. The problem is, there might be other users or applications using Excel at the same time. Just keep in mind if you're willing to do this.Greave
C
7

Killing Excel is not always easy; see this article: 50 Ways to Kill Excel

This article takes the best advice from Microsoft (MS Knowlege Base Article) on how to get Excel to quit nicely, but then also makes sure about it by killing the process if necessary. I like having a second parachute.

Make sure to Close any open workbooks, Quit the application and Release the xlApp object. Finally check to see if the process is still alive and if so then kill it.

This article also makes sure that we don't kill all Excel processes but only kills the exact process that was started.

See also Get Process from Window Handle

Here is the code I use: (works every time)

Sub UsingExcel()

    'declare process; will be used later to attach the Excel process
    Dim XLProc As Process

    'call the sub that will do some work with Excel
    'calling Excel in a separate routine will ensure that it is 
    'out of scope when calling GC.Collect
    'this works better especially in debug mode
    DoOfficeWork(XLProc)

    'Do garbage collection to release the COM pointers
    'http://support.microsoft.com/kb/317109
    GC.Collect()
    GC.WaitForPendingFinalizers()

    'I prefer to have two parachutes when dealing with the Excel process
    'this is the last answer if garbage collection were to fail
    If Not XLProc Is Nothing AndAlso Not XLProc.HasExited Then
        XLProc.Kill()
    End If

End Sub

'http://msdn.microsoft.com/en-us/library/ms633522%28v=vs.85%29.aspx
<System.Runtime.InteropServices.DllImport("user32.dll", SetLastError:=True)> _
    Private Shared Function GetWindowThreadProcessId(ByVal hWnd As IntPtr, _
    ByRef lpdwProcessId As Integer) As Integer
End Function

Private Sub ExcelWork(ByRef XLProc As Process)

    'start the application using late binding
    Dim xlApp As Object = CreateObject("Excel.Application")

    'or use early binding
    'Dim xlApp As Microsoft.Office.Interop.Excel

    'get the window handle
    Dim xlHWND As Integer = xlApp.hwnd

    'this will have the process ID after call to GetWindowThreadProcessId
    Dim ProcIdXL As Integer = 0

    'get the process ID
    GetWindowThreadProcessId(xlHWND, ProcIdXL)

    'get the process
    XLProc = Process.GetProcessById(ProcIdXL)


    'do some work with Excel here using xlApp

    'be sure to save and close all workbooks when done

    'release all objects used (except xlApp) using NAR(x)


    'Quit Excel 
    xlApp.quit()

    'Release
    NAR(xlApp)

End Sub

Private Sub NAR(ByVal o As Object)
    'http://support.microsoft.com/kb/317109
    Try
        While (System.Runtime.InteropServices.Marshal.ReleaseComObject(o) > 0)
        End While
    Catch
    Finally
        o = Nothing
    End Try
End Sub
Caespitose answered 23/7, 2013 at 3:53 Comment(1)
This was the only solution that worked for me. Thanks.Duty
D
4

I met the same problems and tried many methods to solve it but doesn't work. Finally , I found the by my way. Some reference enter link description here

Hope my code can help someone future. I have been spent more than two days to solve it. Below is my Code:

//get current in useing excel
            Process[] excelProcsOld = Process.GetProcessesByName("EXCEL");
            Excel.Application myExcelApp = null;
            Excel.Workbooks excelWorkbookTemplate = null;
            Excel.Workbook excelWorkbook = null;
try{
    //DO sth using myExcelApp , excelWorkbookTemplate, excelWorkbook
}
catch (Exception ex ){
}
finally
            {
                //Compare the EXCEL ID and Kill it 
                Process[] excelProcsNew = Process.GetProcessesByName("EXCEL");
                foreach (Process procNew in excelProcsNew)
                {
                    int exist = 0;
                    foreach (Process procOld in excelProcsOld)
                    {
                        if (procNew.Id == procOld.Id)
                        {
                            exist++;
                        }
                    }
                    if (exist == 0)
                    {
                        procNew.Kill();
                    }        
                }
            }
Dionnadionne answered 16/3, 2017 at 9:42 Comment(0)
L
4

I have found that it is important to have Marshal.ReleaseComObject within a While loop AND finish with Garbage Collection.

static void Main(string[] args)
{
    Excel.Application xApp = new Excel.Application();
    Excel.Workbooks xWbs = xApp.Workbooks;
    Excel.Workbook xWb = xWbs.Open("file.xlsx");

    Console.WriteLine(xWb.Sheets.Count);

    xWb.Close();
    xApp.Quit();

    while (Marshal.ReleaseComObject(xWb) != 0);
    while (Marshal.ReleaseComObject(xWbs) != 0);
    while (Marshal.ReleaseComObject(xApp) != 0);

    GC.Collect();
    GC.WaitForPendingFinalizers();
}
Latium answered 29/6, 2020 at 18:25 Comment(1)
Instead of calling ReleaseComObject() in a while-loop, you could also call FinalReleaseComObject() once. That will reset the reference counter to 0 without the need for a loop. Please make sure you only do this when you're sure that there can be no other references to the COM object.Tumultuous
F
2

You may kill process with your own COM object excel pid

add somewhere below dll import code

[DllImport("user32.dll", SetLastError = true)]
private static extern int GetWindowThreadProcessId(IntPtr hwnd, ref int lpdwProcessId);

and use

 if (excelApp != null)
            {
                int excelProcessId = -1;
                GetWindowThreadProcessId(new IntPtr(excelApp.Hwnd), ref excelProcessId);

                Process ExcelProc = Process.GetProcessById(excelProcessId);
                if (ExcelProc != null)
                {
                    ExcelProc.Kill();
                }
            }
Floating answered 17/1, 2018 at 19:14 Comment(0)
D
1
excelBook.Close();
excelApp.Quit();

add end of the code, it could be enough. it is working on my code

Deibel answered 21/7, 2013 at 22:37 Comment(1)
Yes, but when I use it, save dialog appears, and I don't want it appears :(Unmoved
C
1
         wb.Close();
         app.Quit();

         System.Diagnostics.Process[] process = System.Diagnostics.Process.GetProcessesByName("Excel");
         foreach (System.Diagnostics.Process p in process)
         {
             if (!string.IsNullOrEmpty(p.ProcessName) && p.StartTime.AddSeconds(+10) > DateTime.Now)
             {
                 try
                 {
                     p.Kill();
                 }
                 catch { }
             }
         }

It Closes last 10 sec process with name "Excel"

Corposant answered 27/5, 2015 at 8:5 Comment(0)
G
1

The right way to close all excel process

var _excel = new Application();
foreach (Workbook _workbook in _excel.Workbooks) {
    _workbook.Close(0);
}

_excel.Quit();
_excel = null;

Using process example, this may close all the excel process regardless.

var process = System.Diagnostics.Process.GetProcessesByName("Excel");
foreach (var p in process) {
    if (!string.IsNullOrEmpty(p.ProcessName)) {
        try {
            p.Kill();
        } catch { }
    }
}
Glaciate answered 17/5, 2016 at 10:47 Comment(1)
This will also kill any legitimate instances of excel that are open outside of the VS environment. ie lets say I had another excel worksheet open in the actual excel app, well your code kills that too.Tumultuous
G
1

Use a variable for each Excel object and must loop Marshal.ReleaseComObject >0. Without the loop, Excel process still remain active.

public class test{
        private dynamic ExcelObject;
        protected dynamic ExcelBook;
        protected dynamic ExcelBooks;
        protected dynamic ExcelSheet;

public void LoadExcel(string FileName)
        {
            Type t = Type.GetTypeFromProgID("Excel.Application");
            if (t == null) throw new Exception("Excel non installato");
            ExcelObject = System.Activator.CreateInstance(t);
            ExcelObject.Visible = false;
            ExcelObject.DisplayAlerts = false;
            ExcelObject.AskToUpdateLinks = false;
            ExcelBooks = ExcelObject.Workbooks;
            ExcelBook = ExcelBooks.Open(FileName,0,true);
            System.Runtime.InteropServices.Marshal.GetActiveObject("Excel.Application");
            ExcelSheet = ExcelBook.Sheets[1];
        }
 private void ReleaseObj(object obj)
        {
            try
            {
                int i = 0;
             while(   System.Runtime.InteropServices.Marshal.ReleaseComObject(obj) > 0)
                {
                    i++;
                    if (i > 1000) break;
                }
                obj = null;
            }
            catch 
            {
                obj = null;
            }
            finally
            {
                GC.Collect();
            }
        }
        public void ChiudiExcel() {
            System.Threading.Thread.CurrentThread.CurrentCulture = ci;

            ReleaseObj(ExcelSheet);
            try { ExcelBook.Close(); } catch { }
            try { ExcelBooks.Close(); } catch { }
            ReleaseObj(ExcelBooks);
            try { ExcelObject.Quit(); } catch { }
            ReleaseObj(ExcelObject);
        }
}
Garrow answered 2/8, 2018 at 8:48 Comment(0)
B
1

Most of the methods works, but the excel process always stay until close the appliation.

When kill excel process once it can't be executed once again in the same thread - don't know why.

Buttonball answered 30/9, 2018 at 9:58 Comment(0)
P
1
workbook.Close(0);
excelApp.Quit();

Worked for me.

Protein answered 25/12, 2020 at 5:57 Comment(0)
F
1

Another solution to this problem is to save the ProcessID of the Excel program in which you are working with. Then when you are done with the program, you can specifially kill that Excel process without targeting other excel processes.

I got the solution from this answer. Thought I would share it here

So first, you add these line of code outside of a class method

// The DllImport requires -- Using System.Runtime.InteropServices;
[DllImport("user32.dll", SetLastError = true)]
private static extern int GetWindowThreadProcessId(IntPtr hwnd, ref int lpdwProcessId);

After that,

Now in a method of your choosing, add these lines. These lines discard the specific excel process that you are working with

Modify them to your needs, but the logic is thesame

        if (ExcelApp != null)
        {
            int excelProcessId = 0;

            //your Excel Application variable has access to its Hwnd property
            GetWindowThreadProcessId(new IntPtr(ExcelApp.Hwnd), ref excelProcessId);

            // you need System.Diagnostics to use Process Class
            Process ExcelProc = Process.GetProcessById(excelProcessId);

            if (ExcelProc != null)
            {
                ExcelProc.Kill();
            }
        }

Hence in total your program should look like this

class Program
{
    [DllImport("user32.dll", SetLastError = true)]
    private static extern int GetWindowThreadProcessId(IntPtr hwnd, ref int lpdwProcessId);

    static void Main(string[] args)
    {
        Application ExcelApp = new Application();

        _Workbook ExcelWrkBook = ExcelApp.Workbooks.Open(filePath);

        _Worksheet ExcelWrkSht = ExcelWrkBook.ActiveSheet;

        ExcelWrkSht.Cells[1, 2] = "70";

        if (ExcelApp != null)
        {
            int excelProcessId = 0; // simple declare, zero is merely a place holder

            GetWindowThreadProcessId(new IntPtr(ExcelApp.Hwnd), ref excelProcessId);

            Process ExcelProc = Process.GetProcessById(excelProcessId);

            if (ExcelProc != null)
            {
                ExcelProc.Kill();
            }
        }

    }
}

I have tested this and it removes my Excel processes as shown in Task Manager

Fayum answered 19/5, 2021 at 0:36 Comment(0)
C
1
using System;
using Excel = Microsoft.Office.Interop.Excel;

private Excel.Worksheet excelSheet;
private Excel.Workbook wb;
private Excel.Application excel;
public void Close()
{
    wb.Close(true);
    excel.Quit();
    System.Runtime.InteropServices.Marshal.ReleaseComObject(excel);
    System.Runtime.InteropServices.Marshal.ReleaseComObject(wb);
    if(excelSheet!=null)
       System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet);
    GC.Collect();
}

Call GC collect to force a garbage collection after closing the application and workbook.

Clearwing answered 1/8, 2022 at 9:10 Comment(0)
S
1

I'm going to write what worked for me in case it can help somebody in the future.

Doing this before Save, SaveAs AND Quit fixed my problem. It might be needed around other methods I don't use.

m_pXLApp->PutDisplayAlerts( VARIANT_FALSE );

No more dangling EXCEL.EXE

From what I'm understanding from Microsoft documentation, this was designed to silence messages prompting the user to save his work but also works for other messages like compatibility issues.

Those messages never showed up for me but something was preventing Excel from exiting. After trying to track ref counts on COM objects, this was the only "longshot" I had left to try.

Hope this helps somebody

Selfdevotion answered 28/3, 2023 at 16:59 Comment(0)
S
0

Based on another solutions. I have use this:

IntPtr xAsIntPtr = new IntPtr(excelObj.Application.Hwnd);
excelObj.ActiveWorkbook.Close();

System.Diagnostics.Process[] process = System.Diagnostics.Process.GetProcessesByName("Excel");
                foreach (System.Diagnostics.Process p in process)
                {
                    if (p.MainWindowHandle == xAsIntPtr)
                    {
                        try
                        {
                            p.Kill();
                        }
                        catch { }
                    }
                }

Using the "MainWindowHandle" to identify the process and close him.

excelObj: This is my Application Interop excel objecto

Selfpossessed answered 16/5, 2017 at 22:51 Comment(0)
G
0

We can close the Excel Application while converting xls to xlsx by using following code. When we perform this kind of task then Excel application is running in task manager, we Should close this excel which is running in background. Interop is a Com component ,to release the com component we used Marshal.FinalReleaseComObject.

 private void button1_Click(object sender, EventArgs e)
    {

        Excel03to07("D:\\TestExls\\TestExcelApp.XLS");

    }
    private void Excel03to07(string fileName)
    {
        string svfileName = Path.ChangeExtension(fileName, ".xlsx");
        object oMissing = Type.Missing;
        var app = new Microsoft.Office.Interop.Excel.Application();
        var wb = app.Workbooks.Open(fileName, oMissing, oMissing,
                        oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing);
        wb.SaveAs(svfileName, XlFileFormat.xlOpenXMLWorkbook, Type.Missing, Type.Missing, Type.Missing, Type.Missing, XlSaveAsAccessMode.xlNoChange, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing);

        wb.Close(false, Type.Missing, Type.Missing);
        app.Quit();
        GC.Collect();
        Marshal.FinalReleaseComObject(wb);
        Marshal.FinalReleaseComObject(app);
   }
Goldshlag answered 18/8, 2018 at 3:28 Comment(0)
W
0

I came up with a good solution for this issue that works in many use cases.

Check my previous post for code and explanation: https://mcmap.net/q/212173/-cannot-close-excel-exe-after-interop-process

Here is the code:

using System;
using System.Runtime.InteropServices;
using Microsoft.Office.Interop.Excel;

namespace YourNameSpace
{
    public class MicrosoftApplications
    {
        [DllImport("user32.dll")]
        static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);
        public class Excel
        {   
            public Excel()
            {
                Application = new Microsoft.Office.Interop.Excel.Application();
                RegisterExitEvent();
            }

            public Microsoft.Office.Interop.Excel.Application Application;
            
            private void RegisterExitEvent()
            {
                Application.WindowDeactivate -= XlApp_WindowDeactivate;
                Application.WindowDeactivate += XlApp_WindowDeactivate;
            }

            private void XlApp_WindowDeactivate(Workbook Wb, Window Wn)
            {
                Kill();
            }

            public void Kill()
            {
                int pid = 0;
                GetWindowThreadProcessId(Application.Hwnd, out pid);
                if (pid > 0)
                {
                    System.Diagnostics.Process p = System.Diagnostics.Process.GetProcessById(pid);
                    p.Kill();
                }
                Application = null;
                GC.Collect();
                GC.WaitForPendingFinalizers();
            }
        }
    }
}

And you can call it by: YourNameSpace.MicrosoftApplications.Excel xlApp = new YourNameSpace.MicrosoftApplications.Excel();

To exit, either the user closes the window or programmatically you can call xlApp.Kill();

While answered 10/2, 2023 at 19:21 Comment(1)
I would really appreciate someone trying my code out to prove this works great. I only got voted down because I originally posted only a link to my original post on resolving this issue and people got their panties in a bunch over it. I assumed stack exchange would want to keep the same answers under a single thread but I guess I was wrong.While
C
-1
        GetWindowThreadProcessId((IntPtr)app.Hwnd, out iProcessId);
        wb.Close(true,Missing.Value,Missing.Value);
        app.Quit();
        System.Diagnostics.Process[] process = System.Diagnostics.Process.GetProcessesByName("Excel");
        foreach (System.Diagnostics.Process p in process)
        {
            if (p.Id == iProcessId)
            {
                try
                {
                    p.Kill();
                }
                catch { }
            }
        }
}
[DllImport("user32.dll")]

private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

uint iProcessId = 0;

this GetWindowThreadProcessId finds the correct Process Id o excell .... After kills it.... Enjoy It!!!

Corposant answered 16/6, 2015 at 13:37 Comment(0)
W
-1
private void releaseObject(object obj)
{
    try
    {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(obj);
        obj = null;
    }
    catch (Exception ex)
    {
        obj = null;
        MessageBox.Show("Unable to release the Object " + ex.ToString());
    }
    finally
    {
        GC.Collect();
    }
}
Waylen answered 3/2, 2020 at 19:34 Comment(1)
While this code may solve the question, including an explanation of how and why this solves the problem would really help to improve the quality of your post, and probably result in more up-votes. Remember that you are answering the question for readers in the future, not just the person asking now. Please edit your answer to add explanation, and give an indication of what limitations and assumptions apply.Indefeasible

© 2022 - 2024 — McMap. All rights reserved.